avoid-unnecessary-hook-widgets

Path
lint/performance/avoid-unnecessary-hook-widgets
Default
Off, opt in
Origin
Falcon
Rule
164 of 512
flutter_hooks

Flags confirmed hook widgets and HookBuilder callbacks that do not use hooks.

Replace an unused hook widget with a regular widget to simplify its lifecycle. Only concrete, directly package-derived HookWidget and HookConsumerWidget classes with their own build body, and inline HookBuilder callbacks, are checked. Every call except a generative constructor of a confirmed Flutter Widget subtype retains its immediate hook owner. Getter reads and cascades also retain that owner; nested callbacks do not retain an outer owner. Abstract classes, inherited builds, and unresolved hook widget identities are skipped. Classes extended elsewhere in the same library are skipped; subclasses in other libraries are not visible and must be checked before conversion. Indirect hooks through overloaded operators or setters are not tracked.

Invalid

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';

class EmptyBlock extends HookWidget {
  @override
  Widget build(BuildContext context) { return const SizedBox(); }
}

class EmptyArrow extends hooks.HookWidget {
  @override
  Widget build(BuildContext context) => const SizedBox();
}

class EmptyConsumer extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) => const SizedBox();
}

final emptyArrow = HookBuilder(builder: (context) => const SizedBox());
final emptyBlock = hooks.HookBuilder(builder: (context) { return const SizedBox(); });
final explicitNew = new HookBuilder(builder: (context) => const SizedBox());

class OuterWithoutHooks extends HookWidget {
  @override
  Widget build(BuildContext context) => HookBuilder(builder: (context) {
    useState(0);
    return const SizedBox();
  });
}

class CallbackOnly extends HookWidget {
  @override
  Widget build(BuildContext context) => Clickable(onPressed: () { useState(0); });
}

final nestedCallback = hooks.HookBuilder(builder: (context) {
  return Clickable(onPressed: () { hooks.useState(0); });
});

final prefixedNew = new hooks.HookBuilder(builder: (context) => const SizedBox());
final parenthesized = (hooks.HookBuilder(builder: (context) => const SizedBox()));

class Clickable extends StatelessWidget {
  const Clickable({required this.onPressed});
  final void Function() onPressed;
  @override
  Widget build(BuildContext context) => const SizedBox();
}

class PlainFieldRead extends HookWidget {
  const PlainFieldRead(this.child);
  final Widget child;
  @override
  Widget build(BuildContext context) => child;
}
This widget does not use hooks; replace it with a regular widget.
5
6class EmptyBlock extends HookWidget {
This widget does not use hooks; replace it with a regular widget.
10
11class EmptyArrow extends hooks.HookWidget {
This widget does not use hooks; replace it with a regular widget.
15
16class EmptyConsumer extends HookConsumerWidget {
This widget does not use hooks; replace it with a regular widget.
20
21final emptyArrow = HookBuilder(builder: (context) => const SizedBox());
This widget does not use hooks; replace it with a regular widget.
21final emptyArrow = HookBuilder(builder: (context) => const SizedBox());
22final emptyBlock = hooks.HookBuilder(builder: (context) { return const SizedBox(); });
This widget does not use hooks; replace it with a regular widget.
22final emptyBlock = hooks.HookBuilder(builder: (context) { return const SizedBox(); });
23final explicitNew = new HookBuilder(builder: (context) => const SizedBox());
This widget does not use hooks; replace it with a regular widget.
24
25class OuterWithoutHooks extends HookWidget {
This widget does not use hooks; replace it with a regular widget.
32
33class CallbackOnly extends HookWidget {
This widget does not use hooks; replace it with a regular widget.
37
38final nestedCallback = hooks.HookBuilder(builder: (context) {
This widget does not use hooks; replace it with a regular widget.
41
42final prefixedNew = new hooks.HookBuilder(builder: (context) => const SizedBox());
This widget does not use hooks; replace it with a regular widget.
42final prefixedNew = new hooks.HookBuilder(builder: (context) => const SizedBox());
43final parenthesized = (hooks.HookBuilder(builder: (context) => const SizedBox()));
This widget does not use hooks; replace it with a regular widget.
51
52class PlainFieldRead extends HookWidget {

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';

class StateUser extends HookWidget {
  @override
  Widget build(BuildContext context) {
    useState(0);
    return const SizedBox();
  }
}
class ConsumerUser extends HookConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    hooks.useEffect(() {});
    return const SizedBox();
  }
}
Widget useContent() { useState(0); return const SizedBox(); }
Widget _useContent() => useContent();
class CustomHookUser extends HookWidget {
  @override
  Widget build(BuildContext context) => useContent();
}
class PrivateHookUser extends HookWidget {
  @override
  Widget build(BuildContext context) => _useContent();
}
Widget indirectlyUsesHooks() { useState(0); return const SizedBox(); }
class IndirectUser extends HookWidget {
  @override
  Widget build(BuildContext context) => indirectlyUsesHooks();
}
abstract class AbstractWidget extends HookWidget {
  @override
  Widget build(BuildContext context) => const SizedBox();
}
class InheritedBuild extends StateUser {}
final withHook = HookBuilder(builder: (context) {
  useState(0);
  return const SizedBox();
});
final withCustomHook = hooks.HookBuilder(builder: (context) => useContent());
final withIndirectHook = HookBuilder(builder: indirectlyUsesHooksWithContext);
Widget indirectlyUsesHooksWithContext(BuildContext context) => indirectlyUsesHooks();

class MethodHookUser extends HookWidget {
  Widget content() { useState(0); return const SizedBox(); }
  @override
  Widget build(BuildContext context) => content();
}

final prefixedHook = hooks.HookBuilder(builder: (context) {
  hooks.useState(0);
  return const SizedBox();
});
final prefixedNewHook = new hooks.HookBuilder(builder: (context) {
  hooks.useEffect(() {});
  return const SizedBox();
});

class GeneratedHooks extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final states = List.generate(3, (_) => useState(0));
    return const SizedBox();
  }
}

class HookBundle {
  final state = useState(0);
  HookBundle();
  factory HookBundle.create() {
    useState(0);
    return HookBundle();
  }
}
class FactoryHookUser extends HookWidget {
  @override
  Widget build(BuildContext context) {
    new HookBundle.create();
    return const SizedBox();
  }
}
class FieldInitializerHookUser extends HookWidget {
  @override
  Widget build(BuildContext context) {
    HookBundle();
    return const SizedBox();
  }
}

Widget get hookContent { useState(0); return const SizedBox(); }
class TopLevelGetterUser extends HookWidget {
  @override
  Widget build(BuildContext context) => hookContent;
}
class MemberGetterUser extends HookWidget {
  Widget get content { useState(0); return const SizedBox(); }
  @override
  Widget build(BuildContext context) => content;
}
class ExplicitGetterUser extends HookWidget {
  Widget get content { useState(0); return const SizedBox(); }
  @override
  Widget build(BuildContext context) => this.content;
}

class HookActions extends StatelessWidget {
  void init() { useState(0); }
  @override
  Widget build(BuildContext context) => const SizedBox();
}
class CascadeHookUser extends HookWidget {
  @override
  Widget build(BuildContext context) => HookActions()..init();
}

class UnknownCallUser extends HookWidget {
  @override
  Widget build(BuildContext context) {
    Unknown();
    return const SizedBox();
  }
}
class ExplicitUnknownCallUser extends HookWidget {
  @override
  Widget build(BuildContext context) {
    new Unknown();
    return const SizedBox();
  }
}

class HookBase extends HookWidget {
  @override
  Widget build(BuildContext context) => const SizedBox();
}
class HookChild extends HookBase {
  @override
  Widget build(BuildContext context) {
    useState(0);
    return super.build(context);
  }
}

class StaticHooks {
  static Widget content() { useState(0); return const SizedBox(); }
}
class StaticHookUser extends HookWidget {
  @override
  Widget build(BuildContext context) => StaticHooks.content();
}
extension ContextHooks on BuildContext {
  Widget hookContent() { useState(0); return const SizedBox(); }
}
class ExtensionHookUser extends HookWidget {
  @override
  Widget build(BuildContext context) => context.hookContent();
}

class Panel extends StatelessWidget {
  const Panel();
  static Widget withCounter() {
    useState(0);
    return const Panel();
  }
  factory Panel.counted() {
    useState(0);
    return const Panel();
  }
  @override
  Widget build(BuildContext context) => const SizedBox();
}
class StaticWidgetMethodUser extends HookWidget {
  @override
  Widget build(BuildContext context) => Panel.withCounter();
}
class WidgetFactoryUser extends HookWidget {
  @override
  Widget build(BuildContext context) => Panel.counted();
}
class ExplicitWidgetFactoryUser extends HookWidget {
  @override
  Widget build(BuildContext context) => new Panel.counted();
}

class ShadowedWidgetConstructor extends HookWidget {
  Widget SizedBox() {
    useState(0);
    return const Panel();
  }
  @override
  Widget build(BuildContext context) => SizedBox();
}

Configure avoid-unnecessary-hook-widgets

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

falcon.jsonjson
{
  "linter": {
    "rules": {
      "performance": {
        "avoid-unnecessary-hook-widgets": "error"
      }
    }
  }
}

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

example.dartdart
// falcon-ignore lint/performance/avoid-unnecessary-hook-widgets: <reason>