Skip to content

Core Concepts

Understanding these concepts will help you build FluxUI apps confidently.


The WebSocket Reactive Model

Traditional Python web frameworks render HTML on every request (request → HTML → browser). FluxUI uses a persistent WebSocket connection instead:

Browser                              Server
  |                                     |
  |  GET /  (HTTP)                      |
  |------------------------------------>|
  |  <-- HTML shell (no content yet)    |
  |                                     |
  |  WS /ws (upgrade)                   |
  |------------------------------------>|
  |  <-- full_render { html: "..." }    |  Page renders once, sends full HTML
  |                                     |
  |  event { type: "click", id: "b1" } |  User clicks a button
  |------------------------------------>|
  |  <-- component_update { id: "m1" } |  Only the Metric re-renders
  |                                     |
  |  event { type: "input", id: "t1" } |  User types in an input
  |------------------------------------>|
  |  <-- batch_update [...]             |  Multiple components patched at once
  |                                     |

The browser receives targeted HTML patches, not full page refreshes. This makes FluxUI apps feel instant even with complex state.


How ui.state() + render() Works

The reactive system has three parts:

1. ReactiveVar — the source of truth

count = ui.state(0)   # ReactiveVar[int]

A ReactiveVar holds a value and a set of weak references to subscribed components.

2. Render context — automatic subscription tracking

When FluxUI renders a component, it sets a thread-local "render context" (a ContextVar). Any ReactiveVar whose .value or .get() is called during that render automatically adds itself to the context. After rendering, FluxUI diffs the old and new subscription sets to maintain the live subscription graph.

# Inside a render(), reading .value subscribes the component:
ui.Text(lambda: f"Count: {count.value}")
#                              ^^^^ — read inside render context → subscribed

3. Notification and re-render

When you call count.set(...) or count.update(...):

  1. ReactiveVar._notify() iterates the weak-ref subscriber set
  2. Each subscribed component calls _mark_dirty() on itself
  3. The session's flush loop batches all dirty components
  4. Only dirty components are re-rendered and sent as patches

Stable Component IDs

Every component gets a stable ID derived from its position in the component tree and its type. This ID is embedded as a data-flux-id attribute in the HTML. The browser uses it to locate and replace the right element when a patch arrives.

You can also assign explicit IDs:

ui.Text("Hello", id="my-text")

Explicit IDs are useful when writing tests with FluxTestClient.


Session Isolation

Each WebSocket connection creates one Session object. The session parameter in your page function is that object. ui.state() calls inside a page function are executed fresh for each connection, giving every user their own private state:

@app.page("/")
def home(session):
    count = ui.state(0)  # Each session gets its own ReactiveVar instance
    ...

If you need state shared across all sessions (e.g., a live counter), use app.shared_state:

app.shared_state["counter"] = ui.state(0)  # module-level, shared

@app.page("/")
def home(session):
    counter = app.shared_state["counter"]  # same ReactiveVar for everyone
    ui.Text(lambda: str(counter.value))
    ui.Button("+", on_click=lambda: counter.update(lambda v: v + 1))

WebSocket Protocol

FluxUI uses a simple JSON protocol over the WebSocket:

Server → Browser (outbound)

Message type When sent Key fields
full_render On initial connect or route change html (full page HTML)
component_update Single component changed id, html
batch_update Multiple components changed updates: [{id, html}, ...]
navigate Programmatic navigation url, replace
set_theme Theme switch theme ("light" | "dark")
toast Notification popup message, variant, duration

Browser → Server (inbound)

Message type When sent Key fields
connect WS handshake complete route, session_id
event User interaction event_type, component_id, value
navigate Client-side link click url
upload_done File upload completed component_id, paths, names

Component Overrides

You can replace any built-in component with your own implementation:

@ui.override("Button")
class MyButton:
    def render(self, label, *, variant="primary", **kwargs):
        # Custom render logic
        ...

Overrides are registered in a global ComponentRegistry and can be scoped to a single page via the overrides= kwarg on @app.page(...).

See Components — Advanced for the full API.