Hi everyone,
As Streamlit matures, we are moving from simple scripts into the realm of complex internal tools and production applications. However, as app complexity grows, developers face two major hurdles:
-
The DX Nightmare: Managing state with loose
st.session_statestring keys leads to brittle code, namespace collisions, and zero type safety in large projects. -
The UX Gap: Progress is temporary. If a user refreshes the page or shares a link, their “work-in-progress” context is lost.
I built st-page-state to treat application state as a first-class architectural citizen. It replaces loose dictionaries with strict, typed, and URL-aware classes.
Why this is for Complex Apps:
1. Architectural Integrity (The session_state Solution)
-
Declarative Schema: Define your state like a Pydantic model. It brings structure to your app, replacing magic strings with class attributes.
-
Namespace Isolation: Every
PageStateclass has its own isolated namespace. This allows modular teams to build different parts of an app without ever worrying about key collisions inst.session_state. -
Strict Typing: Stop worrying if a value is a string or an int. The library handles automatic type casting from both the UI and the URL.
-
Developer Tooling: Use
.dump()to export your entire app state for API payloads or.reset()to restore a module to its factory settings instantly.
2. UX Persistence (The URL Solution)
-
Deep-Linked Workflows: Every step of a multi-page process is instantly synced to the URL. The URL becomes a persistent pointer to a user’s exact “work-in-progress”.
-
Selfish Persistence (Default: True): When a user interacts with a module, the URL purges stale parameters from other components, ensuring a clean, focused address bar.
-
Automatic URL Restoration: If a user manually deletes a URL parameter, the
restore_url_on_touchfeature automatically repopulates it from thesession_statethe moment that variable is accessed. -
Cross-State Sharing: Use
share_url_withto allow independent modules (like a sidebar and a main workspace) to coexist in the URL simultaneously.
3. The Contract: From Strings to Architecture
Before: Native `st.session_state` (20+ Lines)
# 1. Initialize logic scattered across the script
if "status" not in st.session_state:
# 2. Manual URL parsing
url_val = st.query_params.get("status", "pending")
st.session_state["status"] = url_val
# 3. No type safety
status = st.session_state["status"]
# 4. Manual URL updating on change
def update_status():
st.query_params["status"] = st.session_state["status"]
st.selectbox("Status", ["pending", "active"], key="status", on_change=update_status)
After: With `st-page-state` (3 Lines)
Declarative, typed, and automatically synchronized with the URL.
from st_page_state import PageState, StateVar
# Creates a State class that hold the attribute "status", whenever "status" is touch, it touchs directly on session state.
# Ex: FilterState.status = value, value = FilterState.status
class FilterState(PageState):
status: str = StateVar(default="pending", url_key="status")
# .bind() method returns a dict with key= and on_change=, that sync widget changing with session state and query params.
st.selectbox("Status", ["pending", "active"], **FilterState.bind("status"))
Github: https://github.com/ju-sants/st-page-state
Pypi: pip install st-page-state