avoid_hooks_outside_build
This rule flags a useX() call that happens outside a hook context — anywhere that is not a HookWidget.build, a HookBuilder’s builder, or another hook function.
Why use this rule
Section titled “Why use this rule”flutter_hooks stores hook state in a list attached to the element and matches each call to its slot by position. That bookkeeping only exists while a hook widget is building. Call a hook from an event handler, a lifecycle method, or a plain helper and there is no hook context to write into: you get an exception, or worse, state written into an unrelated widget’s slots.
Together with avoid_conditional_hooks and avoid_misused_hooks, this covers the rules of hooks: call them unconditionally, the same number of times, from a hook context.
See also: flutter_hooks rules
class MyWidget extends HookWidget { @override Widget build(BuildContext context) { return ElevatedButton( onPressed: () { // No hook context inside a callback final counter = useState(0); }, child: const Text('tap'), ); }}class MyWidget extends HookWidget { @override Widget build(BuildContext context) { // Called directly in build — valid final counter = useState(0);
return ElevatedButton( onPressed: () => counter.value++, child: Text('${counter.value}'), ); }}
// Composing hooks inside another hook is valid tooValueNotifier<int> useCounter() { return useState(0);}Other places there is no hook context
Section titled “Other places there is no hook context”A StatelessWidget, not a HookWidget
Section titled “A StatelessWidget, not a HookWidget”The commonest version of this mistake: the hook is in a build method, just
not one that has hook storage behind it.
// Don't — MyWidget is a StatelessWidget, so there is no hook contextclass MyWidget extends StatelessWidget { const MyWidget({super.key});
@override Widget build(BuildContext context) { final counter = useState(0); // LINT return Text('${counter.value}'); }}
// Do — extend HookWidgetclass MyWidget extends HookWidget { const MyWidget({super.key});
@override Widget build(BuildContext context) { final counter = useState(0); return Text('${counter.value}'); }}A lifecycle method
Section titled “A lifecycle method”initState runs once, outside the build pass hooks depend on:
// Don'tclass MyState extends State<MyWidget> { @override void initState() { super.initState(); final controller = useTextEditingController(); // LINT }}Convert the widget to a HookWidget and call the hook in build; the hook
already gives you the once-per-widget behaviour initState was providing.
A plain helper function
Section titled “A plain helper function”// Don't — a top-level function is not a hook contextvoid setupThings() { useState(0); // LINT}
// Do — name it as a hook, and it becomes oneValueNotifier<int> useThings() { return useState(0);}Splitting build across several methods
Section titled “Splitting build across several methods”A widget whose build delegates to helper methods needs those methods named as
hook contexts, or the hooks in them are reported:
rules: avoid_hooks_outside_build: additional_methods: [buildBody]class MyWidget extends HookWidget { const MyWidget({super.key});
@override Widget build(BuildContext context) => buildBody(context);
// Reported by default; accepted with the config above. Widget buildBody(BuildContext context) { final counter = useState(0); return Text('${counter.value}'); }}Known limitations
Section titled “Known limitations”Hooks are recognised by name — an unqualified call whose name matches use followed by an uppercase letter or digit. Calls resolved to the Dart SDK or the flutter and flutter_web_plugins packages are excluded: SDK helpers such as usePathUrlStrategy() are not hooks. Re-exports are checked against the declaring library. A hook renamed to something else is not detected, and other plain functions following that naming convention are still treated as hooks.
A qualified call is never a hook. controller.useSomething() has a receiver, so it is left alone.
A HookBuilder’s builder closure is a valid context. Hooks inside it are not reported; any other closure is.
Turning this rule off
Section titled “Turning this rule off”This rule is in the recommended preset, so it is on with
preset: recommended or preset: opinionated. Add it to preset: core with
avoid_hooks_outside_build: true.
To turn it off:
rules: avoid_hooks_outside_build: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Options
Section titled “Options”many_lints: rules: avoid_hooks_outside_build: additional_methods: [buildBody]rules: avoid_hooks_outside_build: additional_methods: [buildBody]| Option | Type | Default | Description |
|---|---|---|---|
additional_methods |
list of strings | [] |
Further methods of a hook widget treated as valid hook contexts, alongside build |
Related rules
Section titled “Related rules”avoid_misused_hooks— Don’t call hooks inside loops.avoid_conditional_hooks— Never call hooks inside conditionals, loops, or ternaries.avoid_unnecessary_hook_widgets— Don’t extend HookWidget if you never call any hooks.avoid_ref_watch_outside_build— Subscribe only in build; read once everywhere else.