Reflex: Derive UI Values from One State Class Instead of Copying Them
A filterable todo list in Reflex gets stale when you duplicate data across state fields. Keep one State class as the source of truth and derive display values with computed vars.
09 Nov 2026, 04:44 UTC

The stale-list problem
You build a filterable todo list in Reflex. The full list lives in one state field, the search text in another, and a third field holds the filtered results. A user toggles an item done, but the filtered list still shows the old value. Or the search box lags because two handlers are trying to keep copies in sync. The fix is not more event handlers. It is a smaller state model: keep the mutable source data in one place and derive everything the UI displays from it.
The useful takeaway: treat a Reflex rx.State subclass as the single source of truth, and use computed vars for read-only values that depend on that truth.
One State class holds the source data
In Reflex, plain attributes on an rx.State subclass are base vars. Handler methods mutate them, and simple assignments often get generated setters such as set_query. That means a controlled input can update state without you writing a custom handler for every keystroke.
Because Reflex changes across releases, confirm the exact decorator names, setter generation, and container update behavior against your installed version before relying on them. The pattern below is stable; the precise API surface is version-sensitive.
Computed vars derive display values
A computed var is a method decorated with @rx.var that returns a value derived from base vars. It should be pure and cheap: no side effects, network calls, randomness, or expensive sorting. In the todo example, the filter itself belongs in a computed var, not in a second list that handlers must maintain.
Worked example: a filterable todo list
Store items as a list of dictionaries and the query as a string. Add a computed visible_items that returns items whose text contains the query after casefolding. Render with rx.foreach and bind the input to the base var.
import reflex as rx
class TodoState(rx.State):
items: list[dict] = [
{"id": 1, "text": "Buy milk", "done": False},
{"id": 2, "text": "Write report", "done": True},
]
query: str = ""
@rx.var
def visible_items(self) -> list[dict]:
q = self.query.casefold()
return [item for item in self.items if q in item["text"].casefold()]
def toggle_done(self, item_id: int):
self.items = [
{**item, "done": not item["done"]} if item["id"] == item_id else item
for item in self.items
]
def index():
return rx.vstack(
rx.input(value=TodoState.query, on_change=TodoState.set_query),
rx.foreach(
TodoState.visible_items,
lambda item: rx.hstack(
rx.checkbox(
is_checked=item["done"],
on_change=lambda _: TodoState.toggle_done(item["id"]),
),
rx.text(item["text"]),
),
),
)
Typing in the input fires an event to the server, updates TodoState.query, and re-renders components that depend on visible_items. Toggling a checkbox passes the item id into toggle_done, so list rows stay stateless and there is no duplicate selection state to drift.
Trade-offs and limitations
Computed vars are convenient, but they can recompute whenever dependencies change. If a derivation is expensive, check whether your Reflex version supports caching, precompute it in a background event, or move the work to a data store.
Reflex state is session-scoped and server-side, but state values are serialized to the browser. Treat state vars as potentially client-visible: do not put secrets or sensitive PII in plain state fields. Session state is per-user UI state, not a shared database or system of record for multi-user data.
Verify the pattern in your app
Run pip show reflex in your terminal inside the active Python environment to confirm the installed version. No elevated permissions are needed; the expected check is a version string. If you run it outside the project environment, it may report a different or missing package.
Start the app with reflex run from the project root where your Reflex config lives. The expected check is a local dev server that compiles without errors. The main risk is a version mismatch between the CLI and generated project files.
Once running, type in the filter box and confirm the list narrows without a full page reload. Open browser devtools and inspect the network or state payload to confirm only intended state vars are sent to the client. If your layout allows importing the state class, call toggle_done in a small Python test and assert that both the base var and the derived value change together.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.