MvdD
MvdD

Reputation: 23496

Session state is reset in Streamlit multipage app

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

Answers (3)

trey hannam
trey hannam

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

Holm
Holm

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

MathCatsAnd
MathCatsAnd

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.

  1. A hacky solution (not my preference) is to recommit values to session state at the top of every page.
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.

  1. My preferred solution is to think of widget keys as separate from the values I want to keep around. I usually adopt the convention of prefixing widget keys with an underscore.
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

Related Questions