Hooks reference
All hooks are methods on &mut ntui::Hooks, handed to every component
render. Hook identity is call order — the same rule React follows: call
hooks unconditionally, in the same order, on every render. Calling more hooks
than the previous render panics (next_slot); calling fewer panics too
(render_fiber). Don’t call hooks inside if/loops/after early returns.
use_state
fn use_state<T: 'static>(&mut self, init: impl FnOnce() -> T) -> State<T>
Owned, per-fiber state. init runs once, on first mount. The returned
State<T> is Clone (cheap — it’s a handle) and Send if T: Send, so it
can be moved into use_input closures and tokio::spawned tasks.
let count = hooks.use_state(|| 0i32);
count.set(5);
count.update(|n| *n += 1);
let current = count.get(); // requires T: Clone
set/update mark the owning fiber dirty, scheduling a re-render. Locking
recovers from poisoning, so a panic inside an update closure can’t
permanently brick the state cell.
use_effect
fn use_effect<D: PartialEq + 'static, C: Into<Cleanup>>(
&mut self,
deps: D,
effect: impl FnOnce() -> C + 'static,
)
Runs effect after mount, and again whenever deps changes (compared with
PartialEq) between renders. Return () for no cleanup, or any FnOnce()
(via Into<Cleanup>) to run before the next effect invocation or on unmount.
hooks.use_effect(id.clone(), move || {
let sub = subscribe(&id);
move || sub.cancel() // Cleanup
});
use_input
fn use_input(&mut self, handler: impl FnMut(KeyEvent, &mut InputCtx) + 'static)
Registers a handler for crossterm KeyEvents routed to this component.
InputCtx::stop_propagation() prevents the event from reaching handlers
registered by ancestor components.
hooks.use_input(move |ev, ctx| match ev.code {
KeyCode::Esc => { ctx.stop_propagation(); app.exit(); }
_ => {}
});
use_paste
fn use_paste(&mut self, handler: impl FnMut(&str, &mut InputCtx) + 'static)
Registers a handler for bracketed-paste events (both render and
render_inline enable bracketed paste on enter). The pasted text arrives
whole, once per paste — unlike a paste on a terminal without bracketed
paste, which arrives as a burst of key events where each newline is
indistinguishable from an Enter press. Same deepest-first dispatch and
stop_propagation() semantics as use_input.
hooks.use_paste(move |text, _ctx| {
input.update(|b| b.push_str(&text.replace('\n', " ")));
});
use_future / use_stream
fn use_future<Fut: Future<Output = ()> + Send + 'static>(&mut self, make: impl FnOnce() -> Fut)
fn use_stream<S: Stream + Send + 'static>(
&mut self,
make: impl FnOnce() -> S + Send + 'static,
on_item: impl FnMut(S::Item) + Send + 'static,
)
Spawns tokio work owned by the component (tokio::spawn), aborted
automatically on unmount. The future/stream runs on the (Send) task, so it
can only talk back to the component through State<T> handles cloned in
before the move — never through borrows, since the fiber tree itself is
!Send. use_stream is sugar over use_future that polls a Stream and
calls on_item per item.
let f = frame.clone();
hooks.use_future(move || async move {
loop {
tokio::time::sleep(Duration::from_millis(120)).await;
f.update(|n| *n = n.wrapping_add(1));
}
});
use_task
fn use_task<D: PartialEq + 'static, Fut: Future<Output = ()> + Send + 'static>(
&mut self,
deps: D,
make: impl FnOnce() -> Fut + 'static,
)
use_future’s deps-keyed sibling: spawns make() after commit, and whenever
deps changes between renders aborts the current task and spawns a fresh
one (also aborted on unmount). Use it when the spawned work is a function of
an input that can change — a timer keyed on a Duration prop, an animation
driver keyed on its target — so stale work stops instead of racing its
replacement. Same communication rules as use_future: talk back through
State<T> handles only.
let f = fired.clone();
hooks.use_task(duration, move || async move {
let Some(d) = duration else { return };
tokio::time::sleep(d).await;
f.set(true);
});
use_interval / use_tween
fn use_interval(&mut self, period: Duration, on_tick: impl FnMut() + Send + 'static)
fn use_tween(&mut self, target: f32, duration: Duration) -> f32
use_interval calls on_tick every period for as long as the component
stays mounted, starting after the first period elapses (one spawned task,
aborted on unmount). use_tween animates toward target (ease-out-cubic),
returning the current interpolated value each render; retargeting continues
smoothly from wherever the value currently is, and its internal ~60Hz driver
task runs only while the animation is in flight. If a timer closure panics,
the task dies silently and the timer stops — same caveat as use_future.
use_theme / use_focus_scope / use_focusable
fn use_theme(&mut self) -> Theme
fn use_focus_scope(&mut self) -> FocusScopeHandle
fn use_focusable(&mut self) -> Focus
The widget layer’s shared hooks (ntui::widgets). use_theme reads the
nearest provided [Theme] (falling back to the built-in default), so custom
components can match the first-party widgets’ colors. use_focus_scope
creates a Tab/Shift-Tab focus registry — provide the returned handle via
ContextProvider — and use_focusable registers the calling component in
the nearest scope, reporting is_focused() and offering claim(). See the
widgets guide for the composition pattern.
use_context / ContextProvider
fn use_context<T: 'static>(&mut self) -> Option<Rc<T>>
Reads the nearest ancestor ContextProvider value for T, if any, provided
via a Provider element in the tree above. Read at render time: because
reconciliation is synchronous per frame, a provider update and a consumer’s
re-render are consistent within a single frame — but a consumer that skips
re-rendering entirely (its own props-equal fast path) won’t observe a context
change until something else marks it dirty.
use_terminal_size
fn use_terminal_size(&mut self) -> (u16, u16)
Reactive (columns, rows); the component re-renders on terminal resize.
use_memo
fn use_memo<D, T>(&mut self, deps: D, f: impl FnOnce() -> T) -> T
where D: PartialEq + 'static, T: Clone + 'static
Computes f on the first render and again only when deps change.
A component re-renders for many reasons — a keystroke, a parent’s state change, a resize — and most of them do not affect any given derived value. Without a memo, work proportional to the input runs on every one of those renders even when the input has not moved.
Keep deps small: the point is to spend a cheap comparison instead of an
expensive recompute. For a large shared payload, depend on its identity
rather than its contents by wrapping it in Shared — and note
that a Shared left at its Default allocates a fresh pointer each render,
which defeats the comparison.
The value is cloned out on every render, so T should be cheap to clone.
Calling the hook with a different D or T at the same slot panics with
the component’s name, like every other hook’s order check.
let matches = hooks.use_memo(
(props.rows.clone(), props.query.clone()),
|| Shared::new(filter(&props.rows, &props.query)),
);
use_list_selection
fn use_list_selection(&mut self) -> State<ListSelection>
A cursor and viewport over a list, persisted across renders.
ListSelection is plain data — index and offset — with move_by,
to_start, to_end, clamp, and visible. Length and viewport height are
passed per call rather than stored, because both change independently of the
selection: the list is refreshed from elsewhere and the height follows the
terminal.
clamp is the one to remember. Lists refreshed from outside routinely shrink
under a cursor that was valid a moment ago; without it the cursor points past
the end and the viewport scrolls to blank space.
visible returns the range to render — slice with it before building any
row, so a frame costs what is on screen rather than what is in the list. It
pairs with TableProps::viewport.
use_scroll
fn use_scroll(&mut self) -> Scroll
A scroll position for an Overflow::Scroll View — pass a clone to that
view’s scroll prop. Layout feeds content/viewport heights back into the
handle each frame, so the following methods stay clamped:
offset()/max_offset()/at_bottom()scroll_by(delta: i32)— relative (e.g. PgUp =-5, PgDn =5)scroll_to(offset: u16),to_top(),to_bottom()
The view follows new content (stays pinned to the bottom) whenever it’s already scrolled to the bottom — the behavior a chat transcript wants, so streaming replies auto-scroll but a user who’s scrolled back to read history isn’t yanked back down.
use_scrollback
fn use_scrollback(&mut self) -> Scrollback
Only meaningful under render_inline (see
architecture.md). Scrollback::commit(element)
prints element permanently above the live region — it scrolls into the
terminal’s real, mouse-scrollable history. Committed elements should be
static (plain View/Text, no hooks/state — they render once). The typical
pattern: a chat commits each finished turn and drops it from live state,
keeping the live region to just the input/spinner.
Under fullscreen render, commits are queued but never drawn.
use_app
fn use_app(&mut self) -> AppHandle
AppHandle::exit() stops the render loop and returns from render/render_inline.
AppHandle::redraw() requests a redraw without changing any state (rarely
needed — state changes already trigger redraws).
AppHandle::copy_to_clipboard(text) asks the backend to set the system
clipboard via an OSC 52 sequence on the next frame — best-effort, since the
terminal emulator may ignore or gate it (tmux and some terminals require
opt-in). Note this is programmatic copy; mouse selection already works
without it because ntui never captures the mouse.
Testing hooks
RuntimeHandle::test_handle() gives a fiber tree + wake receiver without a
full runtime loop, for unit-testing a hook in isolation. For anything
spanning render → input → frame, prefer ntui::testing::TestTerminal
(see getting-started.md). Async
hook tests typically use #[tokio::test(start_paused = true)] for
deterministic, paused-clock time control over use_future/use_stream
timers.