Skip to content

avoid_hooks_outside_build

v0.8.0WarningConfigurableHook Rules

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.

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 too
ValueNotifier<int> useCounter() {
return useState(0);
}

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 context
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
final counter = useState(0); // LINT
return Text('${counter.value}');
}
}
// Do — extend HookWidget
class MyWidget extends HookWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context) {
final counter = useState(0);
return Text('${counter.value}');
}
}

initState runs once, outside the build pass hooks depend on:

// Don't
class 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.

// Don't — a top-level function is not a hook context
void setupThings() {
useState(0); // LINT
}
// Do — name it as a hook, and it becomes one
ValueNotifier<int> useThings() {
return useState(0);
}

A widget whose build delegates to helper methods needs those methods named as hook contexts, or the hooks in them are reported:

many_lints.yaml
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}');
}
}

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.

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:

many_lints.yaml
rules:
avoid_hooks_outside_build: false

To keep the rule on but skip certain paths, use per-rule exclude.

analysis_options.yaml
many_lints:
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