Skip to content

Reactive State

FluxUI's reactive state system makes it easy to build dynamic UIs. When state changes, only the components that depend on that state re-render — nothing more.


ui.state(initial) — ReactiveVar

The fundamental building block. Creates a reactive variable scoped to the current page render (i.e., to the current session's page call).

count = ui.state(0)          # ReactiveVar[int]
name  = ui.state("Alice")    # ReactiveVar[str]
items = ui.state([1, 2, 3])  # ReactiveVar[list]

Reading the value

count.value   # preferred — reads and subscribes if inside render()
count.get()   # identical to .value

Writing the value

count.set(42)                          # replace the value
count.update(lambda v: v + 1)         # atomic read-modify-write

Peeking without subscription

count.peek()   # reads the current value WITHOUT subscribing the calling component

Use peek() inside on_change handlers to avoid a component self-subscribing to its own input:

name = ui.state("")

def on_change(v):
    # BAD:  name.value — would subscribe this handler's component to 'name'
    # GOOD: name.peek() reads without subscribing, then we set the new value
    if v != name.peek():
        name.set(v)

ui.TextInput(label="Name", value=name, on_change=on_change)

API summary

Method / Property Description
.value Read current value (subscribes if in render context)
.get() Identical to .value
.peek() Read without subscribing
.set(v) Replace value; notifies subscribers if changed
.update(fn) Atomic set(fn(current_value))

ui.computed(fn) — ComputedVar

A derived read-only reactive value that automatically re-evaluates when its dependencies change.

first = ui.state("Alice")
last  = ui.state("Smith")

full_name = ui.computed(lambda: f"{first.value} {last.value}")

ui.Text(lambda: full_name.value)  # re-renders when first or last changes
  • Auto-tracking: ComputedVar runs fn once to discover which ReactiveVar instances it reads, then subscribes to them all.
  • Lazy evaluation: fn is not re-run until .value is read after a dependency changes.
  • Read-only: Calling .set() on a ComputedVar raises AttributeError.

Complex computed values

rows  = ui.list_state([{"name": "Alice", "active": True}, ...])
query = ui.state("")

visible = ui.computed(lambda: [
    r for r in rows.value
    if query.value.lower() in r["name"].lower()
])

ui.DataGrid(data=visible, columns=[{"key": "name", "sortable": True}])

ui.effect(fn) — EffectHandle

Runs a side-effect whenever its reactive dependencies change. The function is called once immediately to capture initial dependencies.

theme = ui.state("light")

handle = ui.effect(lambda: print(f"Theme changed to: {theme.value}"))

Stopping an effect

handle.stop()   # unsubscribes from all dependencies and prevents future runs

Async effects

Currently, effect functions must be synchronous. For async side-effects, use an on_change handler or trigger async work from a button's on_click.


ui.list_state(initial) — ReactiveVar[ReactiveList]

Creates a reactive list where in-place mutations (append, insert, remove, __setitem__, etc.) automatically notify subscribers.

items = ui.list_state(["apple", "banana"])

items.value.append("cherry")    # triggers re-render
items.value[0] = "avocado"      # triggers re-render
del items.value[1]              # triggers re-render

Don't replace the list

Calling items.set([...]) replaces the ReactiveList wrapper, which works but loses the reactive mutation tracking. Prefer mutating in-place.

Example

todo = ui.list_state([])
text = ui.state("")

with ui.Column():
    with ui.Row():
        ui.TextInput(value=text, on_change=text.set, placeholder="New item…")
        ui.Button("Add", on_click=lambda: (todo.value.append(text.peek()), text.set("")))

    for item in todo.value:   # NOTE: read inside lambda/render to subscribe
        ui.Text(item)


ui.dict_state(initial) — ReactiveVar[ReactiveDict]

Creates a reactive dict where key assignment and deletion trigger re-renders.

config = ui.dict_state({"theme": "light", "lang": "en"})

config.value["theme"] = "dark"    # triggers re-render
del config.value["lang"]           # triggers re-render
config.value.update({"theme": "light", "lang": "fr"})  # triggers re-render

Example

filters = ui.dict_state({"status": "all", "sort": "name"})

ui.Select(
    label="Status",
    options=["all", "active", "inactive"],
    value=lambda: filters.value["status"],
    on_change=lambda v: filters.value.__setitem__("status", v),
)


How Auto-Tracking Works

FluxUI uses a ContextVar called _current_render_context to track dependencies:

  1. Before rendering a component, FluxUI sets _current_render_context to a fresh set().
  2. Every ReactiveVar.get() / .value read checks _current_render_context. If it is not None, the variable adds itself to the set.
  3. After rendering, FluxUI compares the new dependency set with the old one and adjusts weakref subscriptions accordingly.
  4. When a ReactiveVar is mutated, it walks its subscriber set and calls _mark_dirty() on each subscribed component. Dirty components are flushed as a batch and their HTML patches are sent over the WebSocket.

Reading state outside render

If you read .value inside an on_click or on_change callback (not during render), no subscription is created. That is intentional — callbacks are not re-run automatically; only render functions are.

count = ui.state(0)

def on_click():
    # Reading .value here does NOT subscribe anything.
    # It just reads the current number.
    print(f"Current count: {count.value}")
    count.set(count.value + 1)

ui.Button("Log count", on_click=on_click)