avoid-hooks-outside-build

Path
lint/correctness/avoid-hooks-outside-build
Default
On (recommended)
Origin
Falcon
Rule
69 of 512
recommendedflutter_hooks

Flags confirmed Flutter hooks outside hook builds, custom hooks, and HookBuilder/HookConsumer builders.

Only resolved flutter_hooks functions and useX/_useX functions that actually compose them count as hooks. Nested callbacks do not inherit the enclosing build or custom-hook permission. Confirmed non-hook widget builds are left to avoid-misused-hooks; unresolved calls, unresolved build ancestry, and shadowed names are ignored.

Invalid

example.dartdart
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_hooks/flutter_hooks.dart' as hooks;

final initial = useState<int>(0).toString();
void start() {
  hooks.useEffect(() {});
}
int useComposed() => useState(0);
void callCustom() {
  useComposed();
}
class Screen extends HookWidget {
  void onPressed() {
    useMemoized(() => 1);
  }
  Widget build(BuildContext context) {
    final callback = () {
      useState(1);
    };
    return HookBuilder(builder: (context) {
      final nested = () {
        useState(2);
      };
      return const SizedBox();
    });
  }
}
void _useFeature() {
  final callback = () {
    useState(3);
  };
}
class Service {
  void build() {
    useState(4);
  }
}
Call hooks directly in a hook widget `build`, custom hook, or `HookBuilder`/`HookConsumer` builder.
4
5final initial = useState<int>(0).toString();
Call hooks directly in a hook widget `build`, custom hook, or `HookBuilder`/`HookConsumer` builder.
6void start() {
7 hooks.useEffect(() {});
Call hooks directly in a hook widget `build`, custom hook, or `HookBuilder`/`HookConsumer` builder.
10void callCustom() {
11 useComposed();
Call hooks directly in a hook widget `build`, custom hook, or `HookBuilder`/`HookConsumer` builder.
14 void onPressed() {
15 useMemoized(() => 1);
Call hooks directly in a hook widget `build`, custom hook, or `HookBuilder`/`HookConsumer` builder.
18 final callback = () {
19 useState(1);
Call hooks directly in a hook widget `build`, custom hook, or `HookBuilder`/`HookConsumer` builder.
22 final nested = () {
23 useState(2);
Call hooks directly in a hook widget `build`, custom hook, or `HookBuilder`/`HookConsumer` builder.
30 final callback = () {
31 useState(3);
Call hooks directly in a hook widget `build`, custom hook, or `HookBuilder`/`HookConsumer` builder.
35 void build() {
36 useState(4);

Valid

example.dartdart
import 'package:flutter/widgets.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_hooks/flutter_hooks.dart' as hooks;
import 'package:hooks_riverpod/hooks_riverpod.dart';

int useFeature() => useState(1);
int _usePrivate() => hooks.useMemoized(() => 2);
int useBusiness() => 3;
void ordinary() { useBusiness(); }
void shadowed(int Function(int) useState) { useState(0); }
void shadowedPrefix(dynamic hooks) { hooks.useState(0); }
class Screen extends HookWidget {
  Widget build(BuildContext context) {
    useState<int>(0);
    useFeature();
    _usePrivate();
    int _useLocal() => hooks.useState<int>(1);
    _useLocal();
    return HookBuilder(builder: (context) {
      hooks.useEffect(() {});
      return const SizedBox();
    });
  }
  int _useMethod() => useState(2);
}
class RiverScreen extends HookConsumerWidget {
  Widget build(BuildContext context, WidgetRef ref) {
    useState(0);
    return const SizedBox();
  }
}
class NonHookScreen extends StatelessWidget {
  Widget build(BuildContext context) {
    useState(0); // Owned by avoid-misused-hooks, not this rule.
    return const SizedBox();
  }
}

class StatefulHooks extends StatefulHookWidget {}
class HooksState extends State<StatefulHooks> {
  Widget build(BuildContext context) {
    useState(0);
    return const SizedBox();
  }
}
class StatefulRiverHooks extends StatefulHookConsumerWidget {}
class RiverHooksState extends ConsumerState<StatefulRiverHooks> {
  Widget build(BuildContext context) {
    useState(0);
    return HookConsumer(builder: (context, ref, child) {
      useState(1);
      return const SizedBox();
    });
  }
}
class UnknownScreen extends MissingHookBase {
  Widget build(BuildContext context) {
    useState(0); // Unresolved ancestry is not proof of an invalid context.
    return const SizedBox();
  }
}
mixin CounterHooks {
  int useCounter() => useState(0);
}
extension ContextHooks on BuildContext {
  int useCounter() => useState(0);
}

mixin HookScreenBuild on HookWidget {
  Widget build(BuildContext context) {
    useState(0);
    return const SizedBox();
  }
}
mixin UnconstrainedBuild {
  Widget build(BuildContext context) {
    useState(0);
    return const SizedBox();
  }
}

Configure avoid-hooks-outside-build

Set its severity in falcon.json. See the configuration guide.

falcon.jsonjson
{
  "linter": {
    "rules": {
      "correctness": {
        "avoid-hooks-outside-build": "error"
      }
    }
  }
}

Or silence one occurrence with a suppression comment; the reason is required.

example.dartdart
// falcon-ignore lint/correctness/avoid-hooks-outside-build: <reason>