Skip to main content

HookCoordinator

Runs a use callback as the body of a HookWidget and passes its result to a builder. It is the most general wrapper in this category: it lets you call hooks inline, anywhere in a widget tree, without declaring a dedicated HookWidget subclass.

class CounterCard extends StatelessWidget {
const CounterCard({super.key});


Widget build(BuildContext context) {
// No dedicated HookWidget subclass: use() runs the hooks, builder consumes them.
return HookCoordinator(
use: () => useState(0),
builder: (count) => TextButton(
onPressed: () => count.value++,
child: Text('Tapped ${count.value} times'),
),
);
}
}

Constructor

HookCoordinator<T>({
Key? key,
required T Function() use,
required Widget Function(T) builder,
});
  • use - a callback run inside the widget's build, so it may call any hooks. Whatever it returns becomes T.
  • builder - receives the value returned by use and produces the widget.

The implementation is exactly builder(use()) inside a HookWidget.build. Because use runs in a hook build, the rules of hooks apply: call hooks unconditionally and in a stable order across rebuilds.

Use cases

  • Using a hook for one small piece of a tree without splitting out a HookWidget - a single useState-backed toggle, a lone useAnimationController driving one widget.

  • Returning a record (or any object) from use to expose several hooks to the builder at once:

    class Greeting extends StatelessWidget {
    const Greeting({super.key, required this.name});

    final String name;


    Widget build(BuildContext context) {
    // Return a record from use() to expose several hooks at once.
    return HookCoordinator(
    use: () {
    final visits = useState(0);
    final greeting = useMemoized(() => 'Hello, $name', [name]);
    return (visits: visits, greeting: greeting);
    },
    builder: (state) => TextButton(
    onPressed: () => state.visits.value++,
    child: Text('${state.greeting} (${state.visits.value})'),
    ),
    );
    }
    }

Every other widget in this category is a specialization of this idea: each owns a specific hook or controller and exposes it through a builder. Reach for a dedicated wrapper when one fits - ComputedStateWrapper for a computed state, TextEditingControllerWrapper for a text field - and use HookCoordinator for the cases that don't have one.

Caveats

  • It is an escape hatch, not the default. For a screen, follow the Screen / State / View pattern - a HookWidget whose state lives in a use<Feature>State hook - rather than inlining logic through a HookCoordinator. Overusing it scatters state-building into the widget tree and undercuts the structure the pattern gives you.

  • Keep use to hook calls and cheap wiring. It runs on every rebuild, so heavy work belongs behind useMemoized / useAutoComputedState inside it, not in the body directly - exactly as in a normal hook build.

See also