Persisting UI State in Reflex Apps with the persist Flag
Learn how Reflex’s persist flag automatically saves component state to localStorage and restores it on page reload, with a worked counter example and notes on limits.
03 Apr 2026, 18:24 UTC

The problem: state vanishes on reload
When you build a Reflex interface, any values stored in a State class disappear as soon as the page is refreshed. Users lose form inputs, counters, or UI toggles, which forces you to either rebuild the state manually or accept a poor experience.
How Reflex persistence works
Reflex offers a persist flag on state variables. When you set persist=True, the framework automatically:
- Serialises the variable’s value to JSON.
- Stores it under a namespaced key in the browser’s
localStorage. - On page load, reads the JSON, reconstructs the value, and hydrates the state while respecting any type hints you declared.
The mechanism runs alongside Reflex’s hot‑reloading dev server: if you add or remove persisted fields, the server merges the new definition with the existing storage, keeping values for fields that still exist and discarding those that no longer match.
Worked example: a persisting counter
Create a fresh Reflex project (or use an existing one) and add the following files.
State definition (state.py)
import reflex as rx
class CounterState(rx.State):
count: int = 0
@rx.var
def display(self) -> str:
return f"Count: {self.count}"
def increment(self):
self.count += 1
# Make the count survive reloads
count: int = rx.field(persist=True)
Component (index.py)
import reflex as rx
from .state import CounterState
def index():
return rx.vstack(
rx.heading(CounterState.display),
rx.button("Increment", on_click=CounterState.increment),
spacing="4",
align_items="center",
padding="2rem",
)
app = rx.App()
app.add_page(index)
app.compile()
To run the example:
- Open a terminal in the project folder.
- Ensure you have Reflex installed (
pip install reflex). - Start the dev server with
reflex run(no special permissions required). - Click the button a few times, note the count, then refresh the page.
After the reload, the displayed count should match the last value you saw. Open the browser’s developer tools → Application → LocalStorage and look for a key similar to reflex_state_<appname>. Its value will be a JSON string containing the persisted count field.
Trade‑offs and limitations
Automatic persistence only works for data that JSON can represent: numbers, strings, booleans, lists, and plain dictionaries. If you need to store a custom class instance, you must supply your own serialize and deserialize callbacks (see Reflex docs for rx.field(serializer=..., deserializer=...)).
Because the data lives in localStorage, it is scoped to the browser origin. Tabs from the same origin share the state, but incognito windows or different ports/protocols will not see it, and the data disappears when the incognito session ends.
Checking the limitation
To verify that a non‑JSON‑serializable field is not persisted, add a variable like data: dict = rx.field(persist=True) where the dict contains a function or a custom object. Reload the page; the field will revert to its default value, and you will see no entry for it in localStorage.
Actionable closing
If your Reflex app needs to remember UI state across reloads, start by marking the relevant State fields with persist=True. Test the behaviour with a simple counter as shown, then expand to more complex structures, keeping in mind the JSON‑serializability rule. For anything beyond primitives, lists, and dicts, implement custom serializers or consider moving the data to a backend store.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.