Multi-page app with session state

@fdubinski, @AteBitHavoc,

Are you still encountering this error after the fix I’ve made recently? Could you post your traceback error to check where the exception is raised?


Hi! Thanks for your precious work. I have this problem: inside this for loop it seems that the session state is not working because it cannot save the values if i turn back on this page after i move to another page.
Do you know why?

Hello @davins90,

Does it raise any exception, or it just fails to save your values silently?

Hey @synode,

thanks for the reply. It just fails to save the results, unfortunately. It works on a single “number_input/selectboc/ecc…” but in this for cycle, it doesn’t. If i move forward on the page of the apps i see the values insert, but if i turning back to the page in which I’ve inserted the values, they disappear. I hope to have explained in a good way my situation.
Thanks

Thanks @synode for the nice demo!

I am running into issues when having different state variables depend on each other. Maybe it’s just a logic error. What I am trying to do is to not have state.input depend on state.selectbox if text is written into the text_input. If a different choice is selected, then state.input should depend on it again. Additionally, a function call should update state.input every time it is called.

My example is the same as your gist, but I changed the main() and page_settings() functions. Here is my code:

import streamlit as st
from streamlit.hashing import _CodeHasher

try:
    # Before Streamlit 0.65
    from streamlit.ReportThread import get_report_ctx
    from streamlit.server.Server import Server
except ModuleNotFoundError:
    # After Streamlit 0.65
    from streamlit.report_thread import get_report_ctx
    from streamlit.server.server import Server


def main():
    state = _get_state()
    pages = {
        "Dashboard": page_dashboard,
        "Settings": page_settings,
    }

    st.sidebar.title(":floppy_disk: Page states")
    page = st.sidebar.radio("Select your page", tuple(pages.keys()))

    # Display the selected page with the session state
    pages[page](state)

    # Mandatory to avoid rollbacks with widgets, must be called at the end of your app
    state.sync()


def page_dashboard(state):
    st.title(":chart_with_upwards_trend: Dashboard page")
    display_state_values(state)


def page_settings(state):
    st.title(":wrench: Settings")
    display_state_values(state)

    st.write("---")
    options = ["", "Hello", "World", "Goodbye"]
    state.selectbox = st.selectbox("Select value.", options, options.index(state.selectbox) if state.selectbox else 0)
    state.input = st.text_input("Set input value.", state.input or "")
    state.input = state.selectbox

    state.function = st.button("Function")
    if state.function:
        state.input = state.input + " function"
        print("Press function")


def display_state_values(state):
    st.write("Selectbox state:", state.selectbox)
    st.write("Input state:", state.input)

    if st.button("Clear state"):
        state.clear()


class _SessionState:

    def __init__(self, session, hash_funcs):
        """Initialize SessionState instance."""
        self.__dict__["_state"] = {
            "data": {},
            "hash": None,
            "hasher": _CodeHasher(hash_funcs),
            "is_rerun": False,
            "session": session,
        }

    def __call__(self, **kwargs):
        """Initialize state data once."""
        for item, value in kwargs.items():
            if item not in self._state["data"]:
                self._state["data"][item] = value

    def __getitem__(self, item):
        """Return a saved state value, None if item is undefined."""
        return self._state["data"].get(item, None)
        
    def __getattr__(self, item):
        """Return a saved state value, None if item is undefined."""
        return self._state["data"].get(item, None)

    def __setitem__(self, item, value):
        """Set state value."""
        self._state["data"][item] = value

    def __setattr__(self, item, value):
        """Set state value."""
        self._state["data"][item] = value
    
    def clear(self):
        """Clear session state and request a rerun."""
        self._state["data"].clear()
        self._state["session"].request_rerun()
    
    def sync(self):
        """Rerun the app with all state values up to date from the beginning to fix rollbacks."""

        # Ensure to rerun only once to avoid infinite loops
        # caused by a constantly changing state value at each run.
        #
        # Example: state.value += 1
        if self._state["is_rerun"]:
            self._state["is_rerun"] = False
        
        elif self._state["hash"] is not None:
            if self._state["hash"] != self._state["hasher"].to_bytes(self._state["data"], None):
                self._state["is_rerun"] = True
                self._state["session"].request_rerun()

        self._state["hash"] = self._state["hasher"].to_bytes(self._state["data"], None)


def _get_session():
    session_id = get_report_ctx().session_id
    session_info = Server.get_current()._get_session_info(session_id)

    if session_info is None:
        raise RuntimeError("Couldn't get your Streamlit Session object.")
    
    return session_info.session


def _get_state(hash_funcs=None):
    session = _get_session()

    if not hasattr(session, "_custom_session_state"):
        session._custom_session_state = _SessionState(session, hash_funcs)

    return session._custom_session_state


if __name__ == "__main__":
    main()

Is it possible to implement session state with st.button? It seems to be the only widget that is not supported with this framework at the moment.

Hello @e-tony, welcome to the forum!

Indeed, this is just a matter of logic. Are you expecting something like this? (I’ve removed pages)

from streamlit.hashing import _CodeHasher
from streamlit.report_thread import get_report_ctx
from streamlit.server.server import Server
from random import choices
from string import ascii_letters
import streamlit as st


DATES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]


def main():
    state = _get_state()

    st.write("**Current state:**", state.input)

    predef = st.selectbox("Predefined input", options=DATES)
    custom = st.text_input("Custom input")
    random = "".join(choices(ascii_letters, k=10))

    # This can be a function which changes state.input
    if st.button("Random input"):
        state.input = random

    # Here we check if text input is empty or not
    elif custom:
        state.input = custom

    # If we didn't call our function, and text input is empty, we use predefined choices
    else:
        state.input = predef

    # Mandatory to avoid rollbacks with widgets, must be called at the end of your app
    state.sync()


class _SessionState:

    def __init__(self, session, hash_funcs):
        """Initialize SessionState instance."""
        self.__dict__["_state"] = {
            "data": {},
            "hash": None,
            "hasher": _CodeHasher(hash_funcs),
            "is_rerun": False,
            "session": session,
        }

    def __call__(self, **kwargs):
        """Initialize state data once."""
        for item, value in kwargs.items():
            if item not in self._state["data"]:
                self._state["data"][item] = value

    def __getitem__(self, item):
        """Return a saved state value, None if item is undefined."""
        return self._state["data"].get(item, None)
        
    def __getattr__(self, item):
        """Return a saved state value, None if item is undefined."""
        return self._state["data"].get(item, None)

    def __setitem__(self, item, value):
        """Set state value."""
        self._state["data"][item] = value

    def __setattr__(self, item, value):
        """Set state value."""
        self._state["data"][item] = value
    
    def clear(self):
        """Clear session state and request a rerun."""
        self._state["data"].clear()
        self._state["session"].request_rerun()
    
    def sync(self):
        """Rerun the app with all state values up to date from the beginning to fix rollbacks."""

        # Ensure to rerun only once to avoid infinite loops
        # caused by a constantly changing state value at each run.
        #
        # Example: state.value += 1
        if self._state["is_rerun"]:
            self._state["is_rerun"] = False
        
        elif self._state["hash"] is not None:
            if self._state["hash"] != self._state["hasher"].to_bytes(self._state["data"], None):
                self._state["is_rerun"] = True
                self._state["session"].request_rerun()

        self._state["hash"] = self._state["hasher"].to_bytes(self._state["data"], None)


def _get_session():
    session_id = get_report_ctx().session_id
    session_info = Server.get_current()._get_session_info(session_id)

    if session_info is None:
        raise RuntimeError("Couldn't get your Streamlit Session object.")
    
    return session_info.session


def _get_state(hash_funcs=None):
    session = _get_session()

    if not hasattr(session, "_custom_session_state"):
        session._custom_session_state = _SessionState(session, hash_funcs)

    return session._custom_session_state


if __name__ == "__main__":
    main()

Hello @mkhorasani,

The persisting equivalent to buttons are checkboxes IMO. And buttons doesn’t take a value parameter.
How do you imagine (or plan to use) session state with buttons?

Thanks a lot for the example @synode!

I will play around with that and see if I run into any issues.

I hacked together a more type-safe example for us enterprise-y types :stuck_out_tongue:

6 Likes

Thanks for the idea of assigning default values from variables stored session! The SessionState file works great too. I can you explain the sync() function a bit.

Thanks again!

It’s a requirement to sync back the state at the end of each page refresh, so as to tell the session “keep these values when Streamlit reruns the page”. Otherwise they would be lost like all other values, as there’s not really a traditional main loop otherwise to keep track of state.

In my posted example, I’ve just wrapped it as a decorator to show how you can bury it a bit.