useKeyed
Runs a block of hooks inside a nested context that is rebuilt from scratch whenever the keys change. This is the primitive that lets you run hooks conditionally or per-item, which the plain rules of hooks forbid.
// useKeyed runs the block in a fresh nested hook context whenever the keys
// change. The hooks inside it are torn down and rebuilt on a key change.
ComputedState<Profile> useUserProfile(String userId) {
return useKeyed([userId], () {
// This hook is disposed and re-created whenever userId changes, so the
// previous user's load is dropped rather than re-keyed.
return useAutoComputedState(() => profileService.load(userId));
});
}
Hooks are matched between builds by their call order, so they cannot live inside an if or a loop - the order would change between builds and the state would be attributed to the wrong hook.
// DON'T
if (isLoggedIn) {
final profile = useAutoComputedState(loadProfile); // <- Changes hook order between builds
}
// DON'T
for (final id in ids) {
useAutoComputedState(() => load(id)); // <- Hook count varies between builds
}
useKeyed lifts that restriction. The block runs in its own nested hook context keyed by the values you pass. The hooks inside it always run in the same order relative to each other, so the rule is still satisfied; when the keys change, the entire nested context is disposed and a fresh one is built. The conditional family below (useIf, useIfNotNull) is built on top of it.
Signature
T useKeyed<T>(HookKeys keys, T Function() block);
keys- aList<Object?>. The block's hook state is kept as long as the keys compare equal (by==, element-wise). When they change, the previous nested context is disposed and the block runs again with brand-new hook state.block- runs during build and may call hooks. Whatever it returns becomes the return value ofuseKeyed.
Use cases
- Tying a group of hooks to a key so they reset together when it changes - for example, dropping a previous user's loaded data instead of re-keying every hook inside by hand.
- As the building block for your own conditional hooks, the way
useIfanduseIfNotNullare built on it.
Caveats
- Keys are diffed with
==, element-wise (the same rule askeysonuseStateanduseMemoized). Passing object instances that lack value equality re-runs the block on every build, disposing and rebuilding the nested hooks each time. - A key change disposes all hook state inside the block. That is the point, but it means any
useStateinside resets and any in-flight async work is dropped on the change. If you only want to reset on a coarser signal, key on that signal rather than on a value that changes often.
useIf
Runs the inner hooks only while condition is true, and returns their result as T? - null while the condition is false. When the condition flips to false the inner hooks are disposed.
// useIf only runs the inner hooks while the condition is true. When the
// condition flips to false the inner hooks are disposed and it returns null.
class ExpandableTile extends HookWidget {
final String id;
const ExpandableTile({super.key, required this.id});
Widget build(BuildContext context) {
final isExpanded = useState(false);
// detailsState is ComputedState<Profile>? - null until expanded.
final detailsState = useIf(
isExpanded.value,
() => useAutoComputedState(() => profileService.load(id)), // <- Runs only while expanded
);
return GestureDetector(
onTap: () => isExpanded.value = !isExpanded.value,
child: Text(detailsState?.valueOrNull?.userId ?? 'Tap to load'),
);
}
}
It is useKeyed([condition], () => condition ? block() : null) - the nested context is keyed on the boolean, so flipping the condition tears the inner hooks down or builds them fresh.
Signature
T? useIf<T>(bool condition, T Function() block);
The result is nullable: it is whatever block returns while condition is true, and null otherwise. useIf has its own reference page with a standalone example.
useIfNotNull
Runs the inner hooks only while value is non-null, passing the unwrapped, non-nullable value to the block. Returns R? - null while value is null.
// useIfNotNull runs the inner hooks only while value is non-null, passing the
// unwrapped value to the block. It re-keys on the null/non-null edge only, not
// on every change of the value.
class SelectionDetails extends HookWidget {
final String? selectedId;
const SelectionDetails({super.key, this.selectedId});
Widget build(BuildContext context) {
final detailsState = useIfNotNull(
selectedId,
(id) => useAutoComputedState(() => profileService.load(id), keys: [id]),
);
return Text(detailsState?.valueOrNull?.userId ?? 'Nothing selected');
}
}
It is useKeyed([value != null], () => value?.let(block)). The key is the null/non-null edge, not the value itself, so the inner hooks are kept across changes from one non-null value to another - they are only rebuilt when value crosses to or from null. Because the block keeps its hook state across those changes, pass the value through keys on the inner hooks (as in the snippet) if they should react to it.
Signature
R? useIfNotNull<T extends Object, R>(T? value, R Function(T) block);
useLet
useLet is deprecated. Its source carries @Deprecated("Confusing behavior, use useIfNotNull or useKeyed instead") - prefer useIfNotNull or useKeyed.
useLet has the same shape as useIfNotNull - it runs the block with the unwrapped value when value is non-null - but it keys the nested context on the value itself, not just on its nullness. Every change of value therefore disposes and rebuilds the inner hooks, which is rarely what you want and is the reason it is deprecated.
// DON'T
final state = useLet(userId, (id) => useAutoComputedState(() => load(id)));
// <- Inner hooks reset on every userId change
// DO
final state = useIfNotNull(userId, (id) => useAutoComputedState(() => load(id), keys: [id]));
// <- Inner hooks kept; they react to id via keys
Signature
R? useLet<T extends Object, R>(T? value, R Function(T) block);
See also
- useIf - the conditional member of this family, with its own example
- useMap - the same idea for a dynamic set of keys: one nested context per key
- useMemoizedIf - when you only need a conditional value, not conditional hooks
- Custom hooks - composing your own hooks on top of these primitives