Skip to content

avoid_misused_hooks

v0.8.0WarningConfigurableHook Rules

This rule flags a useX() call inside a for, for-in, while, or do-while loop, including the for element inside a collection literal.

Hook state is addressed by call position. A hook inside a loop runs as many times as the loop iterates, so the number of hook calls depends on your data. The moment that data changes length, every hook after the loop shifts to a different slot and starts reading state that belongs to another hook.

The symptom is state that appears to jump between unrelated widgets, or a useEffect firing with the wrong dependencies — bugs that are hard to trace back to the loop.

This is the loop half of the rules of hooks; avoid_conditional_hooks covers the branching half.

See also: flutter_hooks rules

class MyWidget extends HookWidget {
const MyWidget(this.items);
final List<String> items;
@override
Widget build(BuildContext context) {
// The hook count changes with items.length
for (final item in items) {
final controller = useTextEditingController(text: item);
}
return const SizedBox();
}
}
class MyWidget extends HookWidget {
const MyWidget(this.items);
final List<String> items;
@override
Widget build(BuildContext context) {
// One hook call, regardless of how many items there are
final controllers = useMemoized(
() => items.map((i) => TextEditingController(text: i)).toList(),
[items],
);
return const SizedBox();
}
}

If each item genuinely needs its own hook state, give each one its own hook widget and let the framework keep the contexts separate:

class ItemRow extends HookWidget {
const ItemRow(this.item, {super.key});
final String item;
@override
Widget build(BuildContext context) {
final controller = useTextEditingController(text: item);
return TextField(controller: controller);
}
}
class MyWidget extends HookWidget {
const MyWidget(this.items, {super.key});
final List<String> items;
@override
Widget build(BuildContext context) =>
Column(children: [for (final item in items) ItemRow(item)]);
}

The same shape hides inside a widget tree, where it is easier to miss:

// Don't — one hook per child, and the count follows the data
Widget build(BuildContext context) {
return Column(
children: [
for (var i = 0; i < items.length; i++) Text('${useState(i).value}'),
],
);
}

Exempting a helper that only looks like a hook

Section titled “Exempting a helper that only looks like a hook”

A function named useX that allocates no hook state is reported anyway, since the rule matches by name. Name it in ignored_names rather than restructuring the loop:

many_lints.yaml
rules:
avoid_misused_hooks:
ignored_names: [useLocalHelper]
// Accepted with the config above
Widget build(BuildContext context) {
for (final item in items) {
useLocalHelper(item);
}
return const SizedBox();
}

ignored_widgets does the same for a whole class, which is worth reaching for when the widget is generated and you cannot edit it:

many_lints.yaml
rules:
avoid_misused_hooks:
ignored_widgets: [GeneratedGrid]

Only loops are detected. A hook placed after an early return shifts positions the same way, but proving that requires flow analysis and is out of scope here.

A closure inside the loop body is not a loop. A hook in a callback allocated per iteration is left to avoid_hooks_outside_build.

A qualified call is never a hook. controller.useSomething() inside a loop is left alone.

Hooks are recognised by name — use followed by an uppercase letter or a digit. A hook aliased to another name is not seen.

This rule is in the recommended preset, so it is on with preset: recommended or preset: opinionated. Add it to preset: core with avoid_misused_hooks: true.

To turn it off:

many_lints.yaml
rules:
avoid_misused_hooks: false

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

analysis_options.yaml
many_lints:
rules:
avoid_misused_hooks:
ignored_names: [useLocalHelper]
ignored_widgets: [GeneratedGrid]
Option Type Default Description
ignored_names list of strings [] Hook names never reported, for useX() helpers that only follow the naming convention
ignored_widgets list of strings [] Widget class names whose hooks are never reported