State
State in Ratatui Kit does not mean “put everything in one store.” Different state has different lifetimes: some follows a component instance, some belongs only to a Provider subtree, some follows one navigation, and only some is process-wide shared state.
To see a complete running example first, start with Atom global state. This page is the reference: it explains where each kind of state belongs and how reactive handles such as State<T> and AtomState<T> work.
Choose scope first
Section titled “Choose scope first”| State | Prefer | Lifetime |
|---|---|---|
| Input draft, list cursor, modal open flag | use_state | Follows component instance |
| Subtree theme, workspace, local service object | ContextProvider + use_state | Follows Provider subtree |
| Temporary context carried by one navigation | RouteState | Follows history entry |
| Current user, global theme, cross-page cache | Atom / use_atom | Follows process |
The more global state becomes, the more carefully it should be used. Atom is not “stronger State”; it is State with a longer lifetime and a wider scope.
use_state
Section titled “use_state”use_state creates component-local reactive state:
The returned State<T> is a copyable handle. You can pass it by value into event closures, child component props, hand-written components, or async tasks:
State<T> requires T: Unpin + Send + Sync + 'static. That constraint lets state handles safely move into background tasks and wake the UI after the task completes.
Reading and writing handles
Section titled “Reading and writing handles”State<T> and AtomState<T> are both based on the same ReactiveHandle, so the read/write APIs are consistent:
| API | Purpose | Failure behavior |
|---|---|---|
get() | Read a Copy value | Panic on borrow conflict |
read() | Get an immutable borrow | Panic on borrow conflict |
try_read() | Try immutable borrow | Return None |
write() | Get mutable borrow and wake after change | Panic on borrow conflict |
try_write() | Try mutable borrow | Return None |
set(value) | Replace value and wake | Silently skip on borrow conflict |
write_no_update() | Mutable borrow without wake | Panic on borrow conflict |
try_write_no_update() | Try mutable borrow without wake | Return None |
set_no_update(value) | Replace value without wake | Silently skip on borrow conflict |
Assertion-style APIs fit normal component logic: borrow conflicts are usually programming errors and should surface quickly. try_* APIs fit optional capabilities, defensive reads inside callbacks, or code that explicitly knows a value may be borrowed.
Writes wake the render loop when the mutable borrow drops:
For Copy numbers, +=, -=, *=, and /= trigger the same change notification:
no_update writes
Section titled “no_update writes”One kind of write should not trigger a UI update: draw-stage state passed to a Ratatui stateful widget.
This write exists because Ratatui’s drawing protocol needs &mut State; it is not a business state change. The real selected-item change should happen in an event handler with write(). Native widget bridge shows this boundary.
try_write_no_update() and set_no_update() are non-panicking / direct-assignment variants on the same path. They fit drawing protocols, test harnesses, or very explicit internal synchronization. Ordinary business state writes should keep using write() / set() so the interface can wake.
Atom is feature-gated; enable atom or full. It is for process-wide shared state: theme, current user, cross-page cache, and global selection.
Atoms are module-level declarations and do not need a macro:
Subscribe inside a component with use_atom:
use_atom(&THEME) returns AtomState<T>. Like State<T>, it can be read, written, copied, and moved into closures. The difference is the wake model: writing an Atom wakes components subscribed to that Atom; components that did not subscribe are not redrawn for that write.
You can also read and write an Atom outside components:
get() is only for T: Copy; for non-Copy values, use THEME.state().read().
Atom subscription and unsubscribe
Section titled “Atom subscription and unsubscribe”Every frame, use_atom calibrates its internal handle to the Atom passed at that hook position. When a component unmounts, or when the same hook position starts subscribing to a different Atom, the waker registered on the old Atom under that component key is removed.
That means this “parameter decides which Atom to subscribe to” pattern is safe, as long as hook call order remains stable:
Do not maintain Atom subscription tables by hand; use_atom handles registration and unsubscription.
Drafts should not go into Atom
Section titled “Drafts should not go into Atom”Characters typed during input usually should not be written to an Atom. In Atom global state:
The draft belongs to the current editor page; the submitted value belongs to the whole app. Writing every keypress into Atom wakes unrelated subscribers frequently and makes canceling an edit awkward.
Context is not Atom
Section titled “Context is not Atom”If data should be visible only inside a subtree, prefer ContextProvider. Common examples include local themes, workspace settings, and page-level service objects.
Context’s value is clear scope and local override. The same type can be overridden in nested Providers, and a child component reads the nearest layer. See Custom provider for the full example.
Do not promote page-level state to Atom just because it needs to travel far. First ask whether it has a natural scope; if it does, Provider is usually clearer.
RouteState is not Atom
Section titled “RouteState is not Atom”Router is provider-scoped state, not a global singleton. RouteState is an optional payload on one history entry, useful for carrying one-shot context when moving from a list page to a detail page:
Plain push(path) / replace(path) clears current route state; only push_with_state / replace_with_state carries state explicitly. This prevents old detail-page payloads from leaking into unrelated pages.
Information required by the URL belongs in path params. One-shot context without URL semantics belongs in RouteState. See Routing for details.
Choosing boundaries
Section titled “Choosing boundaries”| Question | Lean toward |
|---|---|
| Should state disappear when the component unmounts? | use_state |
| Does it belong only to one page or subtree? | ContextProvider + use_state |
| Does it belong only to one navigation? | RouteState |
| Do multiple pages need to read and write the same long-lived business data? | Atom |
| Should a write trigger this component to redraw? | write() / set() |
| Is this only to satisfy a stateful widget protocol during draw? | write_no_update() |
A practical rule: place state in the smallest scope where it works correctly. Promote it to Atom only when multiple independent pages, components, or background tasks need the same long-lived state.