Preserving state across sidebar pages

I’m using the st.sidebar functionality to split my app up into pages. Within a given page, the states of any interactive widgets are preserved (e.g., text entered into a st.text_input field) even if I navigate to a different page and then come back later. However, variables/states that are set on one page aren’t accessible on other pages.

As a simple example, here’s some code with three pages trying to define “A”, define “B”, and then add them together:

import streamlit as st

st.sidebar.title("Pages")
radio = st.sidebar.radio(label="", options=["Set A", "Set B", "Add them"])
if radio == "Set A":
    a = float(st.text_input(label="What is a?", value="0"))
    st.write(f"You set a to {a}")
elif radio == "Set B":
    b = float(st.text_input(label="What is b?", value="0"))
    st.write(f"You set b to {b}")
elif radio == "Add them":
    st.write(f"a={a} and b={b}")
    button = st.button("Add a and b")
    if button:
        st.write(f"a+b={a+b}")

When I run this code in Streamlit, I can set and reset A and B but when I flip to the “Add them” page, I get an exception NameError : name 'a' is not defined.

Any suggestions on how to preserve these variables/states across multiple sidebar pages?

2 Likes

Hey zabzug! You are pretty much asking for a feature we’re in the middle of heavy design discussions about right this moment!

It’s called SessionState (actual name TBD). Basically, it lets you create a state object that persists per user session, i.e. per open browser tab.

We haven’t hashed out all the details for how the feature would work yet, but here’s a prototype implementation that should do the job for you: https://gist.github.com/tvst/036da038ab3e999a64497f42de966a92

And you can use it this way:

import streamlit as st
import SessionState

st.sidebar.title("Pages")
radio = st.sidebar.radio(label="", options=["Set A", "Set B", "Add them"])

session_state = SessionState.get(a=0, b=0)  # Pick some initial values.

if radio == "Set A":
    session_state.a = float(st.text_input(label="What is a?", value=session_state.a))
    st.write(f"You set a to {session_state.a}")
elif radio == "Set B":
    session_state.b = float(st.text_input(label="What is b?", value=session_state.b))
    st.write(f"You set b to {session_state.b}")
elif radio == "Add them":
    st.write(f"a={session_state.a} and b={session_state.b}")
    button = st.button("Add a and b")
    if button:
        st.write(f"a+b={session_state.a+session_state.b}")

I created a Gist that has both of these code snippets inlined, so you can try it out like this:

streamlit run https://gist.githubusercontent.com/tvst/faf057abbedaccaa70b48216a1866cdd/raw/9938e3863a062842ec14dfd0f0545f349e85a023/session_state_example.py
7 Likes

One interesting behavior to call out here is that the state eventually gets overwritten by the widget default. For example, I just ran your Gist example and did the following steps:

  • click Set A in sidebar --> insert 8 and hit enter
  • click Set B --> insert 2 and hit enter
  • click Add them --> values add as expected
  • click Set A in sidebar --> do nothing
  • click Set B in sidebar --> do nothing
  • click Set A in sidebar --> do nothing
  • click Set B in sidebar --> do nothing

On the second time through, the values are reset to their defaults.

Good point.

You can address that by setting the widget default to the state value:

session_state.a = float(st.text_input(label="What is a?", value=session_state.a))

I’ll update the post above and the Gist.

Thanks!

Edit: hmm… I’m experiencing a small bug in the script above, where the first time you edit one of those text boxes the UI resets to 0 but the session state is set correctly. I’ll look into this later today.

Edit #2: I think this has the same underlying cause as bug #218. We’re investigating it, and should have a fix soon.

2 Likes

Very cool, I was looking for something like this. Instead of (or in addition to) having get create a SessionState, could you consider allowing the user to create a Dataclass as state so the code complete in the editor is more helpful? From what I can tell SessionState is just a regular object so it should work.

Edit: Also once this is implemented it would be great if buttons can specify a callback. I know it might not fit 100% with the rest of streamlit’s top down declarative style but right now buttons are not very useful and this would enable a lot of things that are hard right now.

3 Likes

Hi @zabzug

I’m also creating a multiapp and i’m very interested in how to do it. Feel free to share your experiences, study the awesome-streamlit app or reach out.

How do you structure your navigation? I use the radiobuttons as there is currently no menu.

Can I see your multipage app somewhere?

Source: https://github.com/MarcSkovMadsen/awesome-streamlit
WWW: https://awesome-streamlit.azurewebsites.net/ and https://awesome-streamlit.org/

Marc

2 Likes

Hi Marc,

My app is structured very similarly to yours, I’m just using a “selectbox” (dropdown menu) instead of radio buttons. Unfortunately, I can’t share the app since I built it for my job. But yours looks great, keep up the good work!

Zack

3 Likes

@biells: This is a great idea. We’re considering this!

2 Likes

@Adrien_Treuille great news, check my POC in Alternate implementation of session state if you’re interested.

1 Like

Edit: hmm… I’m experiencing a small bug in the script above, where the first time you edit one of those text boxes the UI resets to 0 but the session state is set correctly. I’ll look into this later today.

I really like @thiago’s gist for session state, but I’m experiencing this same bug and I see it as a blocker to using state.

It sounds like the bug is well understood by the Streamlit team already so I’m not bothering to turn my code into a minimal example, but I could if that would be helpful.

1 Like

Is there a solution for this? My usecase is the following:

  1. load a df
  2. be able to preprocess that df, save the state into a new df (the preprocessed data)
  3. visualize the preprocessed data
  4. run a model with the preprocessed data and save the results into a df
  5. visualize the model results in page == ‘Visualize’

I would therefore want to go back and forth between the pages and save the dataframe that is the result of the preprocessing and modelling steps in that particular page.

import streamlit as st
import pandas as pd

st.sidebar.title("Pages")
page = st.sidebar.radio(label="", options=["Preprocess", "Visualize", "Predictive Modelling"])

df = pd.read_csv("data.csv")

if page == "Preprocess":
    # custom preprocessing function, create new df
    df = preprocess(df)
elif page == "Visualize":
    # visualize function
    visualize(df)
elif page == "Predictive Modelling":
    # do the modelling, save into df
    df = modelling(df)
2 Likes

I am using the “multi-select” widget within the sidebar and running into the same issue (even when attempting to use the SessionState work around). Each time the code runs the “multi-select” widget presents the default options in the sidebar even though that may not be the data presented on the screen to the user. The multi-select widget works properly when it is not included in the sidebar.

any more updates from anyone on this issue?

Hello @vnguyendc,

The streamlit team may have some updates to share, but in the meantime, I managed to get a working prototype here: Multi-page app with session state

This code currently throws this error with streamlit version 0.63.1.

AttributeError: 'Server' object has no attribute '_session_infos'

Hello @Codeguyross,

Ah, the example uses an outdated version of his session states which has been fixed. Save the original session state code into SessionState.py file, and follow the usage:

>>> import SessionState
>>>
>>> session_state = SessionState.get(user_name='', favorite_color='black')
>>> session_state.user_name
''
>>> session_state.user_name = 'Mary'
>>> session_state.favorite_color
'black'
Since you set user_name above, next time your script runs this will be the
result:
>>> session_state = get(user_name='', favorite_color='black')
>>> session_state.user_name
'Mary'

It is tiresome to do that for every element of the app.