Skip to main content

useFocusNode

Creates a FocusNode that is disposed automatically when the hook context unmounts. It returns the node, ready to attach to a focusable widget.

class SearchField extends HookWidget {
const SearchField({super.key});


Widget build(BuildContext context) {
final focusNode = useFocusNode(debugLabel: 'search'); // <- Created once, disposed for you

return Row(
children: [
Expanded(child: TextField(focusNode: focusNode)),
IconButton(
icon: const Icon(Icons.search),
onPressed: focusNode.requestFocus, // <- Drive focus imperatively from a callback
),
],
);
}
}

Signature

FocusNode useFocusNode({
String? debugLabel,
FocusOnKeyCallback? onKey,
FocusOnKeyEventCallback? onKeyEvent,
bool skipTraversal = false,
bool canRequestFocus = true,
bool descendantsAreFocusable = true,
});

The parameters mirror the FocusNode constructor. They are watched: changing any of them on a later build updates the existing node in place rather than recreating it, so the focus state survives a rebuild.

Use cases

  • Any widget that owns a FocusNode and would otherwise need a StatefulWidget to create and dispose it - text fields, custom focusable widgets, keyboard-shortcut targets.
  • Moving focus imperatively in response to user actions: call node.requestFocus() from the callback that should grab focus (a "next" button, a successful validation), and node.unfocus() to dismiss the keyboard.

Caveats

  • Drive focus imperatively from the callback that changes the underlying condition. Don't mirror an external boolean into focus with an effect - that is the same desync bug useFieldState exists to avoid for text, and it fights the node's own state.

    // DON'T - effect-driven focus stomps on the node's own state
    useEffect(() {
    if (shouldFocus) focusNode.requestFocus();
    else focusNode.unfocus();
    return null;
    }, [shouldFocus]);

    // DO - request focus from the callback that sets the condition
    onPressed: () {
    submit();
    nextFieldFocusNode.requestFocus(); // <- imperative, at the moment it should move
    }
  • This is effectively useMemoized(FocusNode.new, [], (it) => it.dispose()) with every constructor argument forwarded and pushed onto the live node on change. Prefer the dedicated hook over a hand-written useMemoized so the property updates and disposal stay correct.

  • The node is created once and reused across rebuilds. Don't construct a FocusNode inline in the build and pass it to a widget - it would leak and reset focus on every build; that is exactly what this hook prevents.

See also