Multi-page apps with widget state preservation. The simple way

Hi all,

Here are some skeleton code snippets to show the possible use of multi-page apps, with preservation of the widget states.

  • Radiobuttons on the left sidebar. The ‘standard’ Streamlit way, but not so handy for smartphones.
  • Radiobuttons on top of the page. Better suited for smartphones.
  • Normal pushbuttons on top of the page.

2 Likes

Hello @RayJ,

Thank you for submitting multiple use case of multi-page apps!

I noticed you used the following trick to make widget states persistent:

#--- I don't understand the necessity of this line. But it is needed
#    to preserve session_state in the cloud. Not locally.
st.session_state.update(st.session_state)

Unfortunately this will cause an error if you assign a key to a button, download_button, file_uploader or a form.

If you’re interested, here’s a helper function which should deal with this case.

Thanks for the tip, @okld
I will look into your code. I just started using Streamlit a few days ago, so there is still lots to learn.
The st.session_state.update(st.session_state) statement is a mystery to me. It does something under the hood that I can not grasp yet. In a ‘real’ Python program it would not make any sense.
Even more weird is that this line is not needed when run locally. Only when run in the cloud …
[Added ] This last line is false. This statement is also needed when run locally. [/Added]

@okld
I have beent thinking about your remark:

Unfortunately this will cause an error if you assign a key to a button, download_button, file_uploader or a form.

In which case would you need a key for a button? Can’t a callback function (on_click = …) work as an equivalent? I can’t think of a case where you would want to store the state of a button in session_state.

Even more weird is that this line is not needed when run locally. Only when run in the cloud …

That’s strange, I do need this line locally as well. Which version of Streamlit do you use?

In which case would you need a key for a button?

It can be useful if you need, but don’t have access to the button return value, in another function for instance, or if you use multiple buttons with the same label. In that case you must specify different keys to each of them to avoid a Duplicate Widget ID error.

Although using a key with buttons might not be that common, it is required when you use st.form().

That’s strange, I do need this line locally as well. Which version of Streamlit do you use?

1.5.1 locally, online it is 1.5.0.
But I checked again, and you are right. It is also needed locally. This must have been before I modified my code. Sorry for the false alarm.

Although using a key with buttons might not be that common, it is required when you use st.form()

I haven’t used forms yet. But I just read this:

  • st.button and st.download_button cannot be added to a form.

I meant that you must pass a key to st.form().

@okld , check the below code for more weirdness … :wink:
All widgets states are preserved. Including those in the form.
(PS. Never mind the messy code. I just put this together quickly)

#-------------------------------------------------------
# Streamlit demo program to handle multiple pages with widget state
# preservation.
# Page selection: Buttons on the page top.
#
# Ray J.
#-------------------------------------------------------

import streamlit as st

#--- I don't understand the necessity of this line. But it is needed
#    to preserve session_state in the cloud. Not locally.
#st.session_state.update(st.session_state)

#--- Init session_state
if 'active_page' not in st.session_state:
    st.session_state.active_page = 'Home'
    st.session_state.slider1 = 0
    st.session_state.slider2 = 0
    #st.session_state.MyForm = 0
    st.session_state.check1 = False
    st.session_state.check2 = False
    
st.session_state.active_page = st.session_state.active_page
st.session_state.slider1     = st.session_state.slider1
st.session_state.check1      = st.session_state.check1
st.session_state.slider2     = st.session_state.slider2
st.session_state.check2      = st.session_state.check2

#--- Callback functions
def CB_HomeButton():
    st.session_state.active_page = 'Home'

def CB_SliderButton():
    st.session_state.active_page = 'Slider'

def CB_ContactButton():
    st.session_state.active_page = 'Contact'

    
#--- Payload code of each page
def home():
    st.write('Welcome to home page')
    link = '[GitHub](http://github.com)'
    st.markdown(link, unsafe_allow_html=True)
    st.checkbox('Check me', key='check1')
    if st.button('Click Home'):
        st.write('Welcome to home page')

def slider():
    st.write('Welcome to the slider page')
    slide1 = st.slider('this is a slider',min_value=0,max_value=15,key='slider1' )    
    st.write('Slider position:',slide1)
    
def contact():
    st.title('Welcome to contact page')
    st.write(f'Multipage app. Streamlit {st.__version__}')
    if st.button('Click Contact'):
        st.write('Welcome to contact page')
    with st.form(key='MyForm', clear_on_submit=False):
        st.write("Inside the form")
        slider_val = st.slider("Form slider",key='slider2')
        checkbox_val = st.checkbox("Form checkbox", key='check2')

        # Every form must have a submit button.
        submitted = st.form_submit_button("Submit")
        if submitted:
            st.write("slider", slider_val, "checkbox", checkbox_val)

    st.write("Outside the form")


#--- Page selection buttons
col1, col2, col3 = st.columns(3)
col1.button('Home', on_click=CB_HomeButton)
col2.button('Slider', on_click=CB_SliderButton)
col3.button('Contact', on_click=CB_ContactButton)

#--- Run the active page
if   st.session_state.active_page == 'Home':
    home()
elif st.session_state.active_page == 'Slider':
    slider()
elif st.session_state.active_page == 'Contact':
    contact()
st.session_state.active_page = st.session_state.active_page
st.session_state.slider1     = st.session_state.slider1
st.session_state.check1      = st.session_state.check1
st.session_state.slider2     = st.session_state.slider2
st.session_state.check2      = st.session_state.check2

Actually that’s what st.session_state.update(st.session_state) does for every item of your session state, I’m just lazy to specify each one of them like that :stuck_out_tongue:

Internally, Streamlit manages two different states : user-defined states (used when you store values like so: st.session_state.my_state = "hey"), and widget states (when you use a key parameter). These two states work a little bit differently. User-defined states are completely persistent after multiple runs. However if a widget with a key assigned disappear (when your page changes for example), its associated widget state will be cleared.

So to make widget state persistent, the trick is to transform a widget state into a user-defined state. And it is done by self-assigning session state items that were created by a widget, either with st.session_state.update(…), or like you’ve done it in your last example.

In practice, you don’t have to manage those two different states. What Streamlit does instead is, behind the scene, it merges both states into one single object (st.session_state) you can use in your scripts.

For Python, this …:

st.session_state.active_page = st.session_state.active_page
st.session_state.slider1     = st.session_state.slider1
st.session_state.check1      = st.session_state.check1
st.session_state.slider2     = st.session_state.slider2
st.session_state.check2      = st.session_state.check2

would be equivalent to :
st.session_state.update(st.session_state)
But apparantly not for Streamlit…

In my last code snippet above, try to exchange one by the other. The ‘written out’ session_state lines give no error. The .update statement results in an error.

Internally, Streamlit manages two different states : …

Thanks for explaining this. I am beginning to understand. But I can’t help wondering if this isn’t overly complicated. Maybe the user (programmer) should have more control over this. It would certainly help to get a better insight. Now I have the feeling that some ‘un-Pythonic’ stuff is happening under the hood. And it makes me wonder: are there more Streamlit python statements that act un-Pythonic…?

Assuming you only have active_page, slider1, check1, slider2 and check2 in your session state, both code snippets are equivalent. This update() function acts just as you think it will, it works just like a regular python dict update function.

The only difference between session state and a python dict is that there are some rules to fullfill when assigning values to the former (see the last part on “Caveat and limitations”, and this could be interesting as well).

But I can’t help wondering if this isn’t overly complicated. Maybe the user (programmer) should have more control over this. It would certainly help to get a better insight.

On the one hand, I do feel like session state functioning is overly complicated. On the other hand it was developed this way to fit most common usage of session states, and there surely are some technical aspects I have no idea of that led to this implementation.

Now I have the feeling that some ‘un-Pythonic’ stuff is happening under the hood. And it makes me wonder: are there more Streamlit python statements that act un-Pythonic…?

Actually, I think the issue isn’t that Streamlit has un-pythonic behavior under the hood (we shouldn’t care about that), it is that Streamlit wasn’t intended to be used this way. Most apps don’t involve multiple pages with widgets that needs persistence. And if you need Streamlit to support this specific usecase, the steps to follow is to submit a github issue with your feature request.

This st.session_state.update(...) trick remains a trick that wasn’t intended by Streamlit, which could be patched in the next release with no warning, and which does not work in all cases.

If you want to continue your investigation on this matter, I’ve opened an issue a few weeks ago:

1 Like

Thanks very much for your time to explain!