Skip to content

Custom Hook

custom_hook shows the most common business-side custom hook: a command palette’s query, filtered results, cursor clamping, and keyboard handling are all placed in use_command_palette, while the component only receives the returned snapshot to draw the UI.

custom hook demo

This recording is generated by docs/tapes/custom-hook.tape and runs examples/advanced/custom_hook.rs. It types a filter, moves the cursor, submits a command, clears the filter, and submits another command.

cargo run --example custom_hook

Most business custom hooks do not need to implement the low-level Hook trait. If the behavior can be expressed with built-in hooks, directly compose those hooks in a fixed order:

fn use_command_palette(hooks: &mut Hooks, commands: &'static [Command]) -> CommandPalette {
    let query = hooks.use_state(String::new);
    let mut cursor = hooks.use_state(|| 0usize);
    let mut status = hooks.use_state(|| "type to filter commands".to_string());

    let query_text = query.read().clone();
    let query_deps = query_text.clone();
    let visible = hooks.use_memo(
        move || matching_indices(commands, &query_text),
        query_deps,
    );

    let visible_len = visible.len();
    let cursor_value = cursor.get();
    hooks.use_effect(
        move || {
            let next_cursor = if visible_len == 0 {
                0
            } else {
                cursor_value.min(visible_len - 1)
            };
            if next_cursor != cursor_value {
                cursor.set(next_cursor);
            }
        },
        (visible_len, cursor_value),
    );

    hooks.use_event_handler(EventScope::Current, EventPriority::High, move |event| {
        // query / cursor / submit key handling
        EventResult::Consumed
    });

    CommandPalette { /* snapshot */ }
}

Although this function is just an ordinary Rust function, it is still a hook: every frame it calls use_state, use_memo, use_effect, and use_event_handler in a fixed order, then returns the data the component needs to render.

Custom hooks must follow the same rule internally: hook calls cannot be placed inside if, for, match, or early-return branches. This is unsafe:

fn use_bad_palette(hooks: &mut Hooks, enabled: bool) {
    if enabled {
        hooks.use_state(String::new);
    }

    hooks.use_state(|| 0usize);
}

If the first frame has enabled=true and the next frame has enabled=false, the second use_state index shifts and the runtime may retrieve the wrong hook type.

Filtered results are not independent state. They are derived from query and commands:

let query_text = query.read().clone();
let query_deps = query_text.clone();
let visible = hooks.use_memo(
    move || matching_indices(commands, &query_text),
    query_deps,
);

use_memo compares dependencies with PartialEq. Here the dependency is a String: when the query is unchanged, it reuses the previous Vec<usize>; when the query changes, it filters again.

When filtered results shrink, the old cursor can be out of bounds. That correction is a side effect determined by visible_len and the current cursor:

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

This is more reliable than manually correcting the cursor in every key branch. State writes still go through the State handle, so they wake the next redraw.

use_command_palette registers its own keyboard handler. Query, movement, and submit keys return Consumed; keys that do not belong to the command palette, such as q, return Ignored so the outer app can handle them.

hooks.use_event_handler(EventScope::Current, EventPriority::High, move |event| {
    match key.code {
        KeyCode::Char(ch) if !ch.is_control() => {
            query.write().push(ch);
            cursor.set(0);
        }
        KeyCode::Enter => {
            status.set(format!("submitted: {}", command.title));
        }
        _ => return EventResult::Ignored,
    }

    EventResult::Consumed
});

This is the same model as input exclusivity: a hook can register handlers, but it must still be explicit about returning Consumed or Ignored.

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

  • It needs to poll an external future or manual waker in poll_change.
  • It needs to unsubscribe resources in on_drop.
  • It needs to read framework context in pre_component_update / post_component_update.

For ordinary business state, derived data, form input, selection models, and event handling, prefer compositional hooks. They are easier to keep in stable call order and easier to validate in examples and docs.

Next, see the Hooks core model, or continue to Native Widget Bridging to learn how the draw stage connects to ratatui widgets.