avoid_conditional_hooks
v0.4.0 Warning Widget Best Practices
Flags a hook call (useState, useMemoized, useEffect, …) reached only conditionally: inside an if, a ternary, a switch, an if element in a collection, or the right-hand side of && / ||.
The hooks framework tracks state by call order, not by name. Skip a hook on one build and every hook after it shifts position and reads the wrong slot — values swap between hooks, or the build throws. This is the same “Rules of Hooks” constraint as React.
This rule is in the core preset, so it is on with preset: core and every preset above it. No configuration.
See also: flutter_hooks | React Rules of Hooks
class ProfileView extends HookWidget { const ProfileView({required this.userId, super.key});
final String? userId;
@override Widget build(BuildContext context) { // Skipped whenever userId is null — every later hook shifts a slot if (userId != null) { final profile = useMemoized(() => loadProfile(userId!), [userId]); return Text(profile); }
final theme = useState(ThemeMode.system); return Text('${theme.value}'); }}Call the hook unconditionally and put the branch inside it. The call order is then the same on every build:
class ProfileView extends HookWidget { const ProfileView({required this.userId, super.key});
final String? userId;
@override Widget build(BuildContext context) { final profile = useMemoized( () => userId == null ? '' : loadProfile(userId!), [userId], ); final theme = useState(ThemeMode.system);
if (userId == null) return Text('${theme.value}'); return Text(profile); }}Ternaries and && count too
Section titled “Ternaries and && count too”Both branches of a ternary and the right operand of a short-circuit operator are conditionally executed, so hooks there are reported:
// Don'tfinal label = isCompact ? useState('S').value : useState('L').value;final ready = isLoggedIn && useIsMounted()();
// Do — one unconditional call, branch on its valuefinal size = useState(isCompact ? 'S' : 'L');final mounted = useIsMounted();final isReady = isLoggedIn && mounted();Known limitations
Section titled “Known limitations”Loops are not this rule’s job. A hook in a for or while body is reported by avoid_misused_hooks instead — this rule only tracks branches.
The condition of an if or a ternary is not conditional — a hook there runs on every build, so it is left alone.
Hooks inside a nested closure are not reported by this rule; the closure is its own invocation context, and calling a hook from one is avoid_hooks_outside_build’s job. A HookBuilder starts a fresh hook context, so its builder body is checked on its own rather than as part of the enclosing build.
Turning this rule off
Section titled “Turning this rule off”rules: avoid_conditional_hooks: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Related rules
Section titled “Related rules”avoid_unnecessary_hook_widgets— Don’t extend HookWidget if you never call any hooks.avoid_hooks_outside_build— Only call hooks from a hook context.avoid_misused_hooks— Don’t call hooks inside loops.always_pass_global_key— Don’t create a GlobalKey inside build.