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
FocusNodeand would otherwise need aStatefulWidgetto 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), andnode.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
useFieldStateexists to avoid for text, and it fights the node's own state.// DON'T - effect-driven focus stomps on the node's own stateuseEffect(() {if (shouldFocus) focusNode.requestFocus();else focusNode.unfocus();return null;}, [shouldFocus]);// DO - request focus from the callback that sets the conditiononPressed: () {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-writtenuseMemoizedso the property updates and disposal stay correct. -
The node is created once and reused across rebuilds. Don't construct a
FocusNodeinline 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
- useScrollController - the same auto-dispose pattern for a
ScrollController - useFieldState - the source-of-truth field state a focusable text field pairs with
- useGenericFieldState - the same source-of-truth principle, generalized to any field type