Skip to main content
State is the renderer’s memory. It’s a flat key-value store that lives in the browser for the lifetime of your app, visible to every component, writable from every interaction. When state changes, every component that reads from it updates automatically. Because state is centralized and global, components don’t own their data. There’s no local component state, no useState, no callbacks threading data up through a tree. Components declare which keys they depend on, and changes propagate everywhere automatically. Any component anywhere in the tree can read or write any state key.

Providing initial values

State starts empty unless you seed it. For apps running via prefab serve or as a standalone Python script, use set_initial_state(). It returns a proxy object whose attributes are reactive references to the keys you just defined — so you can immediately start using them in components without a separate declaration step:
from prefab_ui.app import set_initial_state

state = set_initial_state(count=0, title="Dashboard", items=[])
state.count       # reactive reference to {{ count }}
state.items       # reactive reference to {{ items }}
For MCP tools and API routes that return a PrefabApp, pass state to the constructor:
from prefab_ui.app import PrefabApp

return PrefabApp(view=view, state={"count": 0, "title": "Dashboard"})
Components with a value prop seed their state key too. Input(name="city", value="London") registers city with an initial value of "London", so no separate set_initial_state call is needed. Either way, those keys are immediately available to every expression in your component tree.

Interactive components as state sources

Interactive components with a name prop are the most natural state source. They automatically sync their current value to that key on every interaction. As you type in the input, the greeting updates on every keystroke. This is a gentle introduction to Prefab’s expression language: double curly braces reference state values, and the pipe operator provides a fallback ("stranger") when the key is undefined. Every interactive control works this way. A Slider(name="volume") writes its position on every drag. A Checkbox(name="agree") writes true/false. A Select(name="size") writes the selected option value. Whatever name you give becomes a key that expressions and actions can reference.

Reading state

Inside component props, {{ key }} template expressions resolve to the current value at render time. Any string prop accepts these expressions, and the renderer re-evaluates them whenever a referenced key changes. Text("{{ count }}") displays the number; Progress(value="{{ volume }}") drives the bar. The expression language itself supports operators and formatting: {{ count + 10 }} adds ten, {{ name | upper }} uppercases. You can write these directly in any string prop. In Python, Rx objects compile to the same {{ key }} expressions in the protocol output, but in your code they behave like Python values — you can apply operators, use them in f-strings, and combine them with other values. The most common way to get an Rx is through the return value of set_initial_state, which is a proxy whose attributes are reactive references to the keys you defined:
from prefab_ui.app import set_initial_state

state = set_initial_state(count=0)

Text(state.count)                            # {{ count }}
Text(f"Count: {state.count}")                # Count: {{ count }}
Text(state.count + 10)                       # {{ count + 10 }}
Text(f"Items: {state.count} total")          # Items: {{ count }} total
The STATE constant works identically but doesn’t require capturing the return value — useful when state is set by actions rather than initialization, or when you want a quick reference without calling set_initial_state first:
from prefab_ui.components import STATE

Text(STATE.count)                            # {{ count }}
For keys not tied to initial state (like let bindings or forward references), use Rx directly: Rx("key"). The full Rx system, including operators, formatting, and the .rx shorthand, is covered in Expressions.

Nested state and dot paths

State values can be nested objects, and you reach into them with dot notation. {{ profile.name }} reads the name field of the profile object. The name prop accepts dot paths too, so inputs can bind directly to nested fields: Integer segments address array items. {{ todos.0.done }} reads the done field on the first item in todos. Inside a ForEach loop, combine this with {{ $index }} to target whichever row the user is interacting with: SetState("todos.{{ $index }}.done", True) checks off the current item. If any segment along a path is missing or the wrong type, the expression resolves to undefined rather than throwing. Reads return undefined gracefully; writes are no-ops with a console warning.

Writing to state

Two things write to state: interactive controls (automatically, via the name prop) and actions (explicitly, in response to events). SetState assigns a value. ToggleState flips a boolean. AppendState and PopState manipulate arrays. CallTool and Fetch can write their results into state via result_key. State is deliberately simple: a flat map with dot-path addressing for nesting. There are no computed properties, no watchers, no derived state in the store itself. Derived values belong in expressions, where they’re computed at render time from whatever state holds. Complex computations belong in Python, run before you return the component tree, or in a server action.