Lifecycle & Updates¶
This guide explains how a Rezi app moves through its lifecycle, how updates are committed, and what “deterministic scheduling” means in practice.
createApp¶
Apps are created via createApp from @rezi-ui/core and a backend from @rezi-ui/node:
import { createApp, ui } from "@rezi-ui/core";
import { createNodeBackend } from "@rezi-ui/node";
type State = { count: number };
const app = createApp<State>({
backend: createNodeBackend(),
initialState: { count: 0 },
config: { fpsCap: 60 },
});
app.view((state) => ui.text(`Count: ${state.count}`));
await app.start();
State machine diagram¶
stateDiagram-v2
[*] --> Created
Created --> Running: start()
Running --> Stopped: stop()
Stopped --> Running: start()
Created --> Disposed: dispose()
Running --> Disposed: dispose()
Stopped --> Disposed: dispose()
Running --> Faulted: fatal error
View vs draw¶
Rezi supports two render modes (exactly one is active):
- Widget mode:
app.view((state) => VNode)— declarative widget tree - Raw mode:
app.draw((g) => void)— low-level draw API escape hatch
Set one mode before calling start(). Switching modes while running is rejected deterministically.
Update patterns¶
State is owned by the app. You update it via app.update(...):
// Functional update (recommended)
app.update((prev) => ({ ...prev, count: prev.count + 1 }));
// Replace state directly
app.update({ count: 0 });
Patterns that keep updates predictable:
- keep
view(state)pure (no I/O, no timers, no mutations) - do work in event handlers, then call
update(...) - derive display values from state inside the view
Commit points¶
Updates are queued and applied at deterministic commit points:
- after a backend event batch is dispatched, and
- after an “explicit user turn” when you call
update()outside event dispatch
That means multiple update() calls in one turn are applied FIFO and result in a single committed state for the next render.
Re-entrancy¶
To prevent hidden feedback loops, the runtime enforces strict rules:
- calling
update()during the render pipeline throwsZRUI_UPDATE_DURING_RENDER - calling app APIs from inside an updater function throws
ZRUI_REENTRANT_CALL
If you need to trigger a follow-up update based on a render result, schedule it from an event handler or effect (composite widget) rather than inside view.
Event handling¶
You can handle events in three layers:
Widget callbacks (recommended)¶
ui.button({
id: "inc",
label: "+1",
onPress: () => app.update((s) => ({ ...s, count: s.count + 1 })),
});
Global keybindings¶
app.keys({
"q": () => app.stop(),
"ctrl+c": () => app.stop(),
"g g": (ctx) => ctx.update((s) => ({ ...s, count: 0 })),
});
Global event stream¶
const unsubscribe = app.onEvent((ev) => {
if (ev.kind === "fatal") console.error(ev.code, ev.message);
});
// later: unsubscribe()
Frame coalescing¶
Rezi coalesces work:
- multiple updates in a single turn produce one commit
- rendering occurs after the commit
- at most one frame is submitted in-flight at a time
This keeps runtime behavior bounded and prevents unbounded “render storms”.
Error handling¶
There are two main classes of errors:
- Synchronous misuse errors: thrown immediately from the API call (invalid state, update during render, etc.)
- Asynchronous fatal errors: emitted to
onEventhandlers as{ kind: "fatal", ... }, then the app transitions toFaultedand the backend is stopped/disposed best-effort
If you register onEvent, treat fatal events as terminal for that app instance.
Runtime error codes¶
Deterministic violations throw ZrUiError with a code:
ZRUI_INVALID_STATE: API called in the wrong app stateZRUI_MODE_CONFLICT: bothviewanddrawconfigured (or conflicting mode)ZRUI_NO_RENDER_MODE:start()called withoutviewordrawZRUI_REENTRANT_CALL: runtime API called re-entrantlyZRUI_UPDATE_DURING_RENDER:update()called during renderZRUI_DUPLICATE_KEY: duplicate VNodekeyamong siblingsZRUI_DUPLICATE_ID: duplicate widgetidfor focus routingZRUI_INVALID_PROPS: widget props failed validationZRUI_PROTOCOL_ERROR: backend protocol parse/validation failureZRUI_DRAWLIST_BUILD_ERROR: drawlist build failureZRUI_BACKEND_ERROR: backend reported a fatal errorZRUI_USER_CODE_THROW: user callback threw an exception
See the API reference for the ZrUiErrorCode type and related helpers.
Cleanup¶
stop()leaves the app in a safe stopped state; you canstart()againdispose()releases resources and is idempotent (terminal state)onEvent(...)returns an unsubscribe function; call it when your app no longer needs the handler
Related¶
- Concepts - Pure view, VNodes, and reconciliation
- Performance - Why coalescing and keys matter
- Node backend - Runtime/backend integration details
Next: Layout.