Reputation: 23496
I'm building a Streamlit multipage application and am having trouble keeping session state when switching between pages. My main page is called mainpage.py and has something like the following:
import streamlit as st
if "multi_select" not in st.session_state:
st.session_state["multi_select"] = ["abc", "xyz"]
if "select_slider" not in st.session_state:
st.session_state["select_slider"] = ("1", "10")
if "text_inp" not in st.session_state:
st.session_state["text_inp"] = ""
st.sidebar.multiselect(
"multiselect",
["abc", "xyz"],
key="multi_select",
default=st.session_state["multi_select"],
)
st.sidebar.select_slider(
"number range",
options=[str(n) for n in range(1, 11)],
key="select_slider",
value=st.session_state["select_slider"],
)
st.sidebar.text_input("Text:", key="text_inp")
for v in st.session_state:
st.write(v, st.session_state[v])
Next, I have another page called 'anotherpage.py' in a subdirectory called 'pages' with this content:
import streamlit as st
for v in st.session_state:
st.write(v, st.session_state[v])
If I run this app, change the values of the controls and switch to the other page, I see the values for the control being retained and printed. However, if I switch back to the main page, everything gets reset to the original values. For some reason st.session_state
is cleared.
Anyone have any idea how to keep the values in the session state? I'm using Python 3.11.1
and Streamlit 1.16.0
Upvotes: 11
Views: 7568
Reputation: 263
I wanted a solution that I could use for multiple components and prevent me from having to initialize a key in advance.
The solution I made will import the following functions via a utils directory. Instead of populating the key, and value named arguments, those will be passed into the maintain_state()
function and it will return a dictionary with KWARGS. I recommend having the on_change argument called write_state()
if you don't want that then writ_state()
can be called at the end of the file, it may not work as smoothly.
# ..\\utils\\session_state.py
from typing import Dict, Any, List, Literal, Optional
def maintain_state(key: str, default_arg_name: Literal["value", "index"], default_value: Any, index_options: Optional[List[Any]] = None) -> Dict[str, Any]:
""" Session state keys generated by widgets such as `st.toggle()` are only kept
when the widget appears on ever page. So, if you make a selection and switch to
a different page your selection will be lost. This is solved by having a permanent
key that exists independent of the widget. Make sure to call write_state() at the
end of any page where maintain_state() is called
Only works for components that return a single value, no multiselect
>>> st.toggle(
label= "Test toggle",
**maintain_state("test_togg", "value", True)
)
>>> maintain_state("test_togg", "value", True)
{
"key": "test_togg_temp",
"value": True
}
"""
kwarg_dict = {
"key": f"{key}_temp",
}
if key_initialized_needed := (key not in st.session_state):
st.session_state[key] = default_value
if default_arg_name == "index" and not key_initialized_needed:
value = index_options.index(st.session_state[key])
else:
value = st.session_state[key]
kwarg_dict[default_arg_name] = value
if default_arg_name == "index":
kwarg_dict["options"] = index_options
return kwarg_dict
def write_state():
"""To make selections permanent the temp keys need to populate the
permanent key
"""
for key in st.session_state:
if key.endswith("_temp"):
permenent_key = key.replace("_temp", "")
st.session_state[permenent_key] = st.session_state[key]
Here's an example of how it would work
# ..main_page.py
from utils.session_state import maintain_state, write_state
import streamlit as st
# {'key': 'test_togg_temp', 'value': True}
st.toggle(
label= "Test toggle",
**maintain_state("test_togg", "value", True),
on_change=write_state()
)
# {'key': 'test_check_temp', 'value': True}
st.checkbox(
label="Test Checkbox",
**maintain_state("test_check", "value", True)
on_change=write_state()
)
# {'key': 'test_box_temp', 'index': 1, 'options': ['A', 'B']}
st.selectbox(
"Test Selectbox",
**maintain_state("test_box", "index", 1, ["A", "B"]),
on_change=write_state()
)
Upvotes: 0
Reputation: 11
There is simplier solution to workaround the issue with a reset for a session state used as a key parameter in a widget. You can avoid using key param but use value param initialized by a value from session state:
import streamlit as st
if "multi_select" not in st.session_state:
st.session_state["multi_select"] = ["abc", "xyz"]
if "select_slider" not in st.session_state:
st.session_state["select_slider"] = ("1", "10")
if "text_inp" not in st.session_state:
st.session_state["text_inp"] = ""
st.session_state["multi_select'] = st.sidebar.multiselect(
"multiselect",
["abc", "xyz"],
value=st.session_state["multi_select"]
)
st.session_state["select_slider"] = st.sidebar.select_slider(
"number range",
options=[str(n) for n in range(1, 11)],
value=st.session_state["select_slider"]
)
st.session_state["text_inp"] = st.sidebar.text_input("Text:", value=st.session_state["text_inp"])
for v in st.session_state:
st.write(v, st.session_state[v])
Session state is not reset in this case.
Upvotes: 1
Reputation: 861
First, it's important to understand a widget's lifecycle. When you assign a key to a widget, then that key will get deleted from session state whenever that widget is not rendered. This can happen if a widget is conditionally not rendered on the same page or from switching pages.
What you are seeing on the second page are the values leftover from the previous page before the widget cleanup process is completed. At the end of loading "anotherpage," Streamlit realizes it has keys in session state assigned to widgets that have disappeared and therefore deletes them.
There are two ways around this.
st.session_state.my_widget_key = st.session_state.my_widget_key
This will interrupt the widget cleanup process and prevent the keys from being deleted. However, it needs to be on the page you go to when leaving a widget. Hence, it needs to be on all the pages.
import streamlit as st
if "multi_select" not in st.session_state:
st.session_state["multi_select"] = ["abc", "xyz"]
if "select_slider" not in st.session_state:
st.session_state["select_slider"] = ("1","10")
if "text_inp" not in st.session_state:
st.session_state["text_inp"] = ""
def keep(key):
# Copy from temporary widget key to permanent key
st.session_state[key] = st.session_state['_'+key]
def unkeep(key):
# Copy from permanent key to temporary widget key
st.session_state['_'+key] = st.session_state[key]
unkeep("multi_select")
st.sidebar.multiselect(
"multiselect",
["abc", "xyz"],
key="_multi_select",
on_change=keep,
args=['multi_select']
)
# This is a edge case and possibly a bug. See explanation.
st.sidebar.select_slider(
"number range",
options=[str(n) for n in range(1, 11)],
value = st.session_state.select_slider,
key="_select_slider",
on_change=keep,
args=["select_slider"]
)
unkeep("text_inp")
st.sidebar.text_input("Text:", key="_text_inp", on_change=keep, args=["text_inp"])
for v in st.session_state:
st.write(v, st.session_state[v])
You will observe I did something different with the select slider. It appears a tuple needs to be passed to the value
kwarg specifically to make sure it initializes as a ranged slider. I wouldn't have needed to change the logic if it was being initialized with a single value instead of a ranged value. For other widgets, you can see that the default value is removed in favor of directly controlling their value via their key in session state.
You need to be careful when you do something that changes a widget's default value. A change to the default value creates a "new widget." If you are simultaneously changing the default value and actual value via its key, you can get some nuanced behavior like initialization warnings if there is ever a conflict.
Upvotes: 6