After a little bit more digging, the behavior of widgets in streamlit state is explainable with these two issues, one of which is an open bug:
- By design, Streamlit today doesn’t provide a built-in way to persist widget state across pages / re-runs. We’re considering a feature to improve this, see [Request for input] Keyed widget state persistence - discussion, possible fixes #6074
- There’s a bug currently where identically defined widgets on different pages will show inconsistent results between what’s displayed and what’s in session_state when you switch pages - Widget output is different than widget state #6146. This is a bug, we should fix it.
I would still like to understand if there is a clean way to detect page changes or tie a function to some kind of “on_page_change” callback, so if anyone has tips on that please leave them as replies here.
[EDIT]
It looks like there is also a fix for #2 that was merged on Aug 31st and should be available in the next release: Identical widgets on different pages are distinct by AnOctopus · Pull Request #7264 · streamlit/streamlit · GitHub
The original post follows below:
[ORIGINAL POST]
Summary
I am trying to understand better how Widgets get their state reset between page changes – I would like to mimic this behavior for my own objects that are getting stored in st.session_state, but am having trouble figuring out how to detect the difference between the user navigating to a new page via the sidebar vs a regular script re-run.
Steps to reproduce
Imagine you have a two-page app
pages/page1.py
import streamlit as st
st.session_state.customstate = "foobar"
input1 = st.text_input("Enter Text", key="input1")
st.write(st.session_state)
st.write(input1)
pages/page2.py
import streamlit as st
input1 = st.text_input("Enter Text", key="input1")
st.write(st.session_state)
st.write(input1)
If you start on page1 and enter a value into the text input “input1”, you will see that st.session_state
contains two elements:
- “input1”: <the text you entered>
- “customstate”: “foobar”
If you then navigate to page2, you will see that “customstate” has persisted but “input1” has not, and the “input1” text box on page2 is empty. This is perhaps counterintuitive to someone less familiar with how session_state
works — since both elements were “stored” in session_state
and your “customstate” persists between pages, why would “input1” not?
I do, however, understand that the internal SessionState
object is more complex than a simple dictionary, and in particular it has separate data stores for widget data vs “user” data.
What makes this confusing is that, if you peek under the hood while the app is running, the underlying SessionState
object still contains an entry for “input1”: <text you entered> in it’s ._old_state
dictionary when page 2 is executed; and the two “input1” widgets on each page have both the same user key AND widget key (the internal name that streamlit generates for each widget). I would expect, therefore, that the item lookup in SessionState.register_widget
would return <the value you entered> when page2 is run – however, it seems like somewhere between register_widget
for the page2 text_input “input1” and the underlying call to SessionState.register_widget
, this value in SessionState._old_state
is getting ignored.
Another thing that confuses me is that this “Widget clearing” behavior seems not to be tied to a specific page hash. If instead of page1 and page2, you have only page1 and you save a change to page1.py
which prompts a page re-run, “input1” retains it’s value even though the page hash has changed.
If someone more familiar with how widget’s with the same key know how to reset session_state between re-runs when the app page changes but not when the page stays the same, I would appreciate it. Also happy to spool up a demo app in the next 24 hours if that would help make my question more clear.
- Streamlit version: 1.26.0
- Python version: 3.11.4
- Using bare venv with pip install streamlit==1.26.0
- OS version: Windows and Linux
- Browser version: