Skip to content

Hooks

Hooks are the connection point between function components and the runtime. A component function runs again every frame; hooks let state, async tasks, event handlers, and context reads attach to the persistent component instance rather than to a temporary Element declaration.

If you have read the counter tutorial, treat this page as a reference: first find the hook you need, then return for ordering, dependency, or custom-hook rules.

NeedHookExample
Component-private reactive stateuse_stateCounter tutorial
Start one async task on mountuse_futureTimer, background polling
Run a synchronous side effect when dependencies changeuse_effectClamp a cursor based on derived data
Run an async side effect when dependencies changeuse_async_effectAsync validation, request refresh
Maintain data / loading / erroruse_async_stateAsync data states
Cache a pure computed valueuse_memoCustom hook
Read a Provider-injected valueuse_context / try_use_contextCustom provider
Register keyboard/mouse eventsuse_event_handlerInput isolation
Register events with hit testinguse_event_handler_with_optionsLocal mouse wheel, clicks inside component area
Declare a modal input layeruse_input_layerModal surface
Read terminal or component sizeuse_terminal_size / use_previous_sizeResponsive layout, previous-frame area measurement
Request application exituse_exitAll exit-capable examples
Clean up when a component unmountsuse_on_dropUnsubscribe external resources
Insert content before the terminal render areause_insert_beforeAdvanced escape hatch for small terminal prefixes

Feature-gated capabilities include use_atom (atom feature) and use_router / use_navigate (router feature). They are covered in State and Routing.

Hooks are indexed by call order. On the first frame, when use_state, use_memo, or use_effect is called, the runtime places the corresponding hook into the component instance’s hook list. On the next frame, it retrieves old hooks in the same order.

Therefore hook calls must not be placed inside if, for, or match branches, and must not come after a path that may return early:

// Do not write this
if enabled {
    hooks.use_state(String::new);
}

let cursor = hooks.use_state(|| 0usize);

If the first frame has enabled=true and the second has enabled=false, the index for cursor shifts and the runtime may read a hook of the wrong type.

The correct pattern is to keep hook order fixed and put conditions in hook arguments, event handlers, or the render tree:

let query = hooks.use_state(String::new);
let cursor = hooks.use_state(|| 0usize);

element!(
    View {
        if enabled {
            Text(text: query.read().clone())
        } else {
            Text(text: "disabled")
        }
    }
)

Control flow inside element! does not affect hook order. See Control flow syntax for complete examples.

use_state creates component-local reactive state. The returned State<T> is a lightweight handle that can be passed by value into closures, child component props, or hand-written component props.

let mut count = hooks.use_state(|| 0i32);

hooks.use_event_handler(EventScope::Current, EventPriority::Normal, move |event| {
    if matches!(event, Event::Key(_)) {
        count += 1;
        return EventResult::Consumed;
    }

    EventResult::Ignored
});

Read state with get() or read(); write with set(), write(), or operators such as +=. Writes wake the render loop so the next frame reruns the component function.

State<T> requires T: Unpin + Send + Sync + 'static, which lets background tasks hold a handle and write state. During draw, if you are only passing state to a Ratatui stateful widget, use write_no_update() so drawing itself does not trigger another update. Native widget bridge shows this boundary.

use_future starts one future when the component mounts. It fits “run this task while the component is alive,” such as a timer loop:

let mut count = hooks.use_state(|| 0);

hooks.use_future(async move {
    loop {
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        count += 1;
    }
});

The use_future initializer is registered only on the first frame. Do not use it for “rerun a request when dependencies change”; use use_async_effect or use_async_state for that.

use_effect means “do something when dependencies change.” Dependencies are compared with PartialEq and do not require Clone:

hooks.use_effect(
    move || {
        let next_cursor = cursor_value.min(visible_len.saturating_sub(1));
        if next_cursor != cursor_value {
            cursor.set(next_cursor);
        }
    },
    (visible_len, cursor_value),
);

use_memo means “reuse this value while dependencies stay equal.” It fits pure computations such as filtering a command list by query:

let visible = hooks.use_memo(
    move || matching_indices(COMMANDS, &query_text),
    query_text,
);

Do not use use_memo for hidden side effects, and do not use use_effect just to cache a side-effect result. The distinction is simple: memo returns a value; effect does work.

use_async_effect replaces the old future and starts a new one when dependencies change:

hooks.use_async_effect(
    async move {
        status.set("checking".to_string());
        // async work
    },
    validation_key,
);

For the common request triad, use use_async_state. It combines use_state + use_async_effect: when dependencies change it sets loading=true, writes data on success, writes error on failure, and sets loading=false at the end.

let result = hooks.use_async_state(
    move || async move { fetch_projects(filter).await },
    filter,
);

let loading = result.loading.get();
let data = result.data.read();
let error = result.error.read();

Old data is retained during refresh. Terminal UIs usually benefit from keeping old content visible while showing loading state, rather than clearing the screen on every refresh.

use_context::<T>() reads the nearest value injected by ContextProvider; try_use_context::<T>() returns None when no provider exists or when the context is currently borrowed.

let (name, accent) = {
    let settings = hooks.use_context::<WorkspaceSettings>();
    (settings.profile.name, settings.profile.accent)
};

Keep context borrows inside small blocks and copy out only the fields needed for rendering. That prevents accidentally holding an old borrow while calling more hooks, entering nested providers, or requesting mutable context.

Providers fit subtree-level capabilities such as theme, current workspace, or local service objects. Use Atom only for cross-page, process-wide state.

use_event_handler registers keyboard/mouse events on the current input layer. A handler must return EventResult:

hooks.use_event_handler(EventScope::Current, EventPriority::Normal, move |event| {
    let Event::Key(key) = event else {
        return EventResult::Ignored;
    };

    match key.code {
        KeyCode::Char('q') => {
            exit();
            EventResult::Consumed
        }
        _ => EventResult::Ignored,
    }
});

Consumed cuts off later handlers; Ignored means this handler did not handle the event, so same-layer or lower-layer handlers may continue.

For events that should work only inside a component’s area, use use_event_handler_with_options:

hooks.use_event_handler_with_options(
    EventScope::Current,
    EventPriority::Normal,
    EventOptions { hit_test: true },
    move |event| {
        // mouse events outside this component's last drawn area are skipped
        EventResult::Ignored
    },
);

hit_test filters only mouse events. Keyboard events have no coordinates and are still delivered normally. The hit area comes from the component’s previous-frame draw area, so do not rely on it for precise mouse interaction before the first draw.

Modal interactions declare an exclusive layer with use_input_layer(open, blocks_lower):

let layer = hooks.use_input_layer(open.get(), true);

hooks.use_event_handler(EventScope::Layer(layer), EventPriority::High, move |event| {
    // modal keys
    EventResult::Consumed
});

InputLayer handles are valid only within the same frame and are reminted every frame. Do not store them in use_state. If you pass the layer to Modal(layer: Some(layer)), Current handlers inside the Modal subtree automatically belong to that layer, and background components stop seeing the same keys.

In a hand-written Component, calling use_event_handler or use_input_layer requires upgrading hooks with a context stack first:

let mut hooks = hooks.with_context_stack(updater.component_context_stack());

Function components get this from #[component] automatically.

use_terminal_size() returns the current terminal size and updates after Resize events. It is one of the few hooks that can be called directly inside a hand-written Component without manually calling with_context_stack.

use_previous_size() returns the component’s previous-frame draw area. It is useful for component logic that needs to know its own region. It is not the final layout result for the current frame; it is the recorded previous-frame area.

use_exit() returns a 'static exit closure. Calling it requests application exit on the next update:

let mut exit = hooks.use_exit();

hooks.use_event_handler(EventScope::Current, EventPriority::Normal, move |event| {
    if matches!(event, Event::Key(key) if key.code == KeyCode::Char('q')) {
        exit();
        return EventResult::Consumed;
    }

    EventResult::Ignored
});

use_on_drop runs a callback when the component is destroyed, which is useful for unsubscribing external resources. Do not use State handles to write UI state inside that callback; the component is already on its unmount path.

use_insert_before() returns an InsertBeforeHandler that can insert extra content before the current terminal render area. It is not part of the layout system and does not create component tree nodes; it is for small terminal-level additions such as IME candidate bars or debug prefixes.

let insert_before = hooks.use_insert_before();

hooks.use_event_handler(EventScope::Current, EventPriority::Normal, move |event| {
    if matches!(event, Event::Key(key) if key.code == KeyCode::Enter) {
        insert_before
            .render_before(Line::from("submitted"), 1)
            .finish();
        return EventResult::Consumed;
    }

    EventResult::Ignored
});

insert_before(height, callback) gives direct access to the lower-level buffer callback. render_before(widget, height) renders a Ratatui Widget into the prefix area. Call finish() afterward to wake the render loop; the queue is handed to the terminal after the next update.

Do not use it for ordinary UI, modals, lists, or page content. Those belong in the component tree with View, Border, Modal, ScrollView, or a custom Component.

Application-level custom hooks should usually be ordinary functions that compose built-in hooks in a stable order:

fn use_command_palette(hooks: &mut Hooks, commands: &'static [Command]) -> CommandPalette {
    let query = hooks.use_state(String::new);
    let cursor = hooks.use_state(|| 0usize);
    let visible = hooks.use_memo(
        move || matching_indices(commands, query.read().as_str()),
        query.read().clone(),
    );

    // event handler / effect...

    CommandPalette { visible, cursor }
}

This function still obeys hook ordering rules. The benefit is a small API surface: the component receives a snapshot for rendering, while state, derived data, and event handling are captured in a well-named use_* function. See Custom hook for the full example.

Implement the lower-level Hook trait only when the capability needs its own lifecycle callbacks, for example:

  • It needs to poll an external future or manual waker in poll_change.
  • It needs runtime context in pre_component_update / post_component_update.
  • It needs to record area or draw-time information in pre_component_draw / post_component_draw.
  • It needs to release external resources in on_drop.

The standard low-level custom-hook shape is Sealed trait + UseXxxImpl + Hooks::use_hook. Start with compositional hooks for ordinary business logic; drop to Hook only when existing hooks cannot express the lifecycle.