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¶
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(...):
ReactiveVar._notify()iterates the weak-ref subscriber set- Each subscribed component calls
_mark_dirty()on itself - The session's flush loop batches all dirty components
- 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:
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.