utopia_hooks vs flutter_hooks
utopia_hooks shares its vocabulary with flutter_hooks - useState, useEffect, useMemoized - and the mental model of state-as-functions is the same. It is not a fork or a superset of flutter_hooks, though: it is a separate implementation built on a different foundation. Do not depend on both. This page covers what changes when you move, and why the differences exist.
Why utopia_hooks
flutter_hooks proved the idea: hooks are a better fit for Flutter local state than StatefulWidget. utopia_hooks takes that same idea and carries it across the whole presentation layer.
- Global state, not just local. flutter_hooks scopes a hook to one widget; sharing state means pairing it with
provider,riverpod, or anInheritedWidget. Here a global state is the same hook construct, registered once and read withuseProvided. - Logic you can unit-test. flutter_hooks ties hooks to widget elements, so the logic in them needs a pumped widget tree to exercise. utopia_hooks runs the same hook under
SimpleHookContextwith no Flutter, which is what makes a clean state/view split pay off. - An architecture, not only a primitive. The decoupled hook context underneath supports the Screen / State / View pattern, global state, and testing as one system - a story flutter_hooks intentionally leaves out of scope.
- Conditional hooks.
useIfanduseMapmake conditional and dynamic hook usage legal, removing the dynamic-builder workarounds plain hooks force.
The migration of everyday hooks is largely a package swap - drop flutter_hooks, add utopia_hooks, change the import. The substance is in the four differences below.
The hook context is decoupled from Flutter
In flutter_hooks, a hook can only run inside the element of a HookWidget - the framework is the hook runtime. utopia_hooks inserts an abstraction layer (HookContext) between hooks and Flutter. HookWidget is one implementation of that context, but not the only one.
That single change is what the rest of the library is built on:
- Hooks can run with no widget tree at all - in a unit test via
SimpleHookContext, or in aHookProviderContainerthat keeps state alive while Flutter is not rendering (app backgrounded, or not yet started). - Global state is a first-class hook construct rather than a separate package.

Global state is part of the library
flutter_hooks scopes a hook to one widget; sharing state across the tree means pairing it with another solution (provider, riverpod, an InheritedWidget). utopia_hooks covers that itself. A global state is a hook registered in a HookProviderContainerWidget and read anywhere with useProvided:
const _providers = <Type, Object? Function()>{
AuthState: useAuthState,
SettingsState: useSettingsState, // <- may useProvided<AuthState>(): registered above
};
A global state can depend on another exactly the way a local hook depends on useState - by calling useProvided at the top. The result is one model for local and global state instead of two libraries with two reactivity systems. See Global state.
The business layer is unit-testable
Because flutter_hooks' hooks are tied to widget elements, logic written in them can only be exercised through a widget test with a pumped tree. utopia_hooks runs the same hook under SimpleHookContext, with no Flutter:
final context = SimpleHookContext(useCounterState);
expect(context().value, 0);
context().onIncrement(); // <- the write rebuilds the hook immediately
expect(context().value, 1);
A state hook becomes as testable as a plain function, which is what makes the Screen / State / View split worthwhile: the State hook holds the logic and is tested directly, the View is dumb. See Unit testing.
Keys semantics, and conditional hooks
Two day-to-day API differences will trip you up if you carry flutter_hooks habits over.
No keys means run once. useEffect takes its keys as an optional positional list that defaults to empty. An empty (or omitted) list means the effect runs a single time, on first build - the equivalent of flutter_hooks passing const [].
// Runs once, on first build:
useEffect(() {
final sub = service.events.listen(onEvent);
return sub.cancel;
}); // <- no keys
// Re-runs whenever `query` changes:
useEffect(() {
search(query);
return null;
}, [query]);
useState, useMemoized, and the async hooks take keys too, with the same rule: change a key and the hook resets or recomputes. (useState returns to its initial value; useMemoized rebuilds its value.)
Conditional hooks exist. The core rule - call the same hooks in the same order every build - still holds, so you cannot put a bare useState behind an if. But utopia_hooks provides nested-context hooks that make conditional and dynamic hook usage legal: useIf runs a block of hooks only when a condition holds, and useMap runs a block per key in a set. These remove the dynamic-builder workarounds (e.g. a variable number of form fields) that flutter_hooks forces.
// Legal: the hooks inside run in their own nested context, gated by the flag.
final preview = useIf(showPreview, () {
final controller = useScrollController();
return buildPreview(controller);
});
What carries over unchanged
The hook rules are the same as flutter_hooks (and are listed in Basics): call hooks directly in a build method or another hook, never in a callback, never conditionally except through the nested hooks above. Most flutter_hooks code that obeys those rules reads identically after the import swap - the differences are additive (global state, testability, conditional hooks), not a reinterpretation of the basics.
Do not add flutter_hooks alongside utopia_hooks. They define their own HookWidget and useState, so mixing them imports two incompatible runtimes. Migrate a file fully and remove the flutter_hooks import.
See also
- Basics - the hook rules, shared with flutter_hooks
- Global state - the capability flutter_hooks leaves to another package
- Unit testing - testing hooks without a widget tree via
SimpleHookContext - useIf - conditional hooks, impossible in plain flutter_hooks
- From StatefulWidget - if you are also leaving
setStatebehind