Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Session.state not working correctly with sliders #3925

Closed
steeley opened this issue Oct 17, 2021 · 9 comments
Closed

Session.state not working correctly with sliders #3925

steeley opened this issue Oct 17, 2021 · 9 comments
Labels
status:awaiting-user-response Issue requires clarification from submitter type:bug Something isn't working

Comments

@steeley
Copy link

steeley commented Oct 17, 2021

Summary

sliders with values in session.state always go back to the initial value between multiple pages even if their session.state value is updated. This was not an issue prior to version 1.0.

Steps to reproduce

create a simple app with 2 pages, a main app with a side bar with radio buttons to select each.

add a session.state counter button on each to show this works ok.
Code snippet: main streamlit app:

 #app.py
import page1
import page2
import streamlit as st

PAGES = { "PAGE1": page1, "PAGE2": page2 }
st.sidebar.title('Navigation')
selection = st.sidebar.radio("Go to", list(PAGES.keys())) 
page = PAGES[selection]
page.app()

Code snippet: page 1

 # page1.py
import streamlit as st 

def app():
 st.title('PAGE1') 
 st.write('Welcome to page1')
 
 if 'count1' not in st.session_state:
    st.session_state.count1 = 1
    
 increment = st.button('Increment')
 if increment:
    st.session_state.count1 += 1

 st.write('count1 = ', st.session_state.count1)
 
 if 'level_1' not in st.session_state:
    st.session_state.level_1 = 1  
            
 st.slider('level1', min_value=0, max_value=10, key = 'level_1')
 st.write(" ")
 st.write('level_1 = ',st.session_state.level_1)


Code snippet: page 2

 # page2.py
import streamlit as st 

def app():
 st.title('PAGE2') 
 st.write('Welcome to page2')
 
 if 'count2' not in st.session_state:
    st.session_state.count2 = 1

 increment = st.button('Increment')
 if increment:
    st.session_state.count2 += 1

 st.write('count2 = ', st.session_state.count2)

 if 'level_2' not in st.session_state:
    st.session_state.level_2 = 1  
            
 st.slider('level2', min_value=0, max_value=10, key = 'level_2')
 st.write(" ")
 st.write('level_2 = ',st.session_state.level_2)

If applicable, please provide the steps we should take to reproduce the bug:

change the slider and counter on page 1
go to page 2, change the slider and counter on page 2
go back to page 1 - counter value ok, slider value back to default
go back to page 2 - counter value ok, slider value back to default

NOTE: selecting 'rerun' from the menu seems ok - all session state data is intact and sliders do not get reset.

Expected behavior:
slider values and visual position should update to new value.

Actual behavior:
slider values and visual position not update to new value.

Is this a regression?

That is, did this use to work the way you expected in the past?
Yes

Debug info

  • Streamlit version: (1.0)
  • Python version: (3.9.7)
  • Using Conda? PipEnv? PyEnv? Pex? - no
  • OS version: OSX 11.6
  • Browser version: Safari 15 and Chrome 94.0.4606.81

Other info::
I tried adding a slider and storing its session.state to your multiapp session.state demo ( release notes, tic tac toe etc)
and got the same error.

@steeley steeley added type:bug Something isn't working status:needs-triage Has not been triaged by the Streamlit team labels Oct 17, 2021
@oerpli
Copy link

oerpli commented Oct 18, 2021

I have a similar issue, though with some differences:

The structure is approx as follows:

# page 1
a = st.multiselect(A, ...)

# page 2
B = f(a)
b = st.multiselect(B, ...)
  • All my selections are made with st.multiselect
  • I have a hierarchy of selections, i.e. the options on page 2 depend on the choice made on page 1
  • When selecting a subset of A and navigating to page 2, state from page 1 is correctly remembered. After selecting some value b from B, state from page 1 is forgotten (i.e. a is not in the state dictionary anymore) and accordingly, the set B = f(a) changes (the function f returns elements from a set that are partitioned by elements in A, i.e. f(x) | f(y) == f(x | y))
  • I tested 0.88, 0.89 and 1.0 and the breaking change was in 0.89, not in 1.0

Edit: Based on the last point I looked at the release notes again and there was a change related to session state:

We've modified session state to reset widgets if any of their arguments changed even if they provide a key.
More information here: https://docs.streamlit.io/library/advanced-features/widget-semantics

I will try to find out how this relates to my issue (it's not obvious as the st.multiselect(A,...) call doesn't have any arguments that change over time. Most likely point 5 in the link above could be the related.

@kmcgrady
Copy link
Collaborator

Hey @steeley and @oerpli. Thanks for the issue! @oerpli is correct, that we did make some adjustments to session state, and specifically widgets in this case. When widgets disappear from the page, we clear out their value in session state.

If you want to maintain this functionality, I would update a variable in session state in a callback. I believe the following code would work.

if 'level_1' not in st.session_state:
    # Initialize to the saved value in session state if it's available
    if 'level_1_slider' in st.session_state:
        st.session_state.level_1 = st.session_state.level_1_slider
    else:
        st.session_state.level_1 = 1  
        st.session_state.level_1_slider = 1  

def handle_change():
   st.session_state.level_1 = st.session_state.level_1_slider 

st.slider('level1', min_value=0, max_value=10, on_change=handle_change, key="level_1_slider")

st.write('level_1 = ', st.session_state.level_1)

This might not be the most ideal for your use case, but we have found most people wanting to isolate widget values in session states as they navigate to new pages. Placing the value into an "independent" variable on session state keeps things maintained across pages.

Feel free to comment if you need any extra help.

@kmcgrady kmcgrady added status:awaiting-user-response Issue requires clarification from submitter and removed status:needs-triage Has not been triaged by the Streamlit team labels Oct 18, 2021
@steeley
Copy link
Author

steeley commented Oct 18, 2021

thanks - that is one way of working around the issue.
However the way you have it now seems inconsistent - the variable attached to the button stays, but the slider one does not.

@kmcgrady
Copy link
Collaborator

In this case, the counter variable is not attached to the button because it is updating a different variable, a consistent behavior. Note the session state variable for the counter has an independent name and not the same as the button's key (which is not supplied).

We debated a lot about the implications of different approaches but felt that this functionality creates the same consistency under the hood. It led us to write more about this behavior in our docs.

@steeley
Copy link
Author

steeley commented Oct 19, 2021

ok thanks for the info.
The overview makes interesting reading.:
"4 and 5 are the most likely to be surprising and may pose a problem for some application designs."
I do love surprises LOL :)

@steeley steeley closed this as completed Oct 19, 2021
@oerpli
Copy link

oerpli commented Oct 19, 2021

The sample you provided did work @kmcgrady , thanks.

I adapted it to reduce the boilerplate a bit, maybe someone has further ideas how it could be improved:

# Helper function that initializes session_state variables
def persist_state(key, default_value=None, set_default=False):
    # Init persist dict
    if "persist" not in st.session_state:
        st.session_state["persist"] = dict()
    # Init key
    if key not in st.session_state["persist"]:
        # Initialize to the saved value in session state if it's available
        if key in st.session_state:
            st.session_state["persist"][key] = st.session_state[key]
        elif default_value is not None:
            st.session_state["persist"][key] = default_value
            st.session_state[key] = default_value

    # Generic callback function (curry with lambda & pass)
    def __handle_change(key):
        st.session_state["persist"][key] = st.session_state[key]

    default = st.session_state["persist"].get(key)
    return {
        "on_change": lambda: __handle_change(key),
        "key": key,
        # only include default arg if set_default=True as some forms don't support default
        **{"default": default for x in [set_default] if set_default},
    }


# Usage
m = st.multiselect("Select from M", M,
     **persist_state("m", [], set_default=True)
)

@okld
Copy link

okld commented Mar 26, 2022

If interested, here's a slightly different solution not relying on on_change, allowing you to use your own callbacks, and working on custom components that don't have a on_change parameter: #4338 (comment)

@riyosan
Copy link

riyosan commented Apr 14, 2022

hi @okld i have another problem about file_uploader, When I apply file_uploader and display the contents of the csv that I uploaded on page A, and I go to page B and when I switch to page A then the file I uploaded earlier will disappear and also not display the contents of the csv

@crxi
Copy link

crxi commented Apr 15, 2022

Hi @riyosan, it seems that it is not possible to set a default value for file_uploader. This line in file_uploader.py explicitly calls check_session_state_rules(default_value=None, key=key, writes_allowed=False). The write_allowed=False bit prevents us from using session_state to set the default value. So even if we have a way to save states between page changes, it seems that we can't set it to the saved values.

PS: I noted that you posted the same issue on crxi/multipage_streamlit#1 but I will answer it here as this seems more of a streamlit restriction and not something multipage_streamlit can get around.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status:awaiting-user-response Issue requires clarification from submitter type:bug Something isn't working
Projects
None yet
Development

No branches or pull requests

6 participants