useMap
Runs the block once per key in a Set, giving each key its own nested hook context. The result is a Map<K, T> from each key to that key's block result. It is the dynamic-set counterpart to useKeyed: where useKeyed keeps one keyed context, useMap keeps one per key.
// One useAutoComputedState instance per id, stable across rebuilds. Adding an
// id starts a fresh load; removing one disposes that instance.
Map<String, ComputedState<Course>> useCourses(Set<String> courseIds) {
return useMap(
courseIds,
(id) => useAutoComputedState(() => courseService.load(id), keys: [id]),
);
}
Running hooks in a loop breaks the rules of hooks - the hook count varies with the collection, so state is attributed to the wrong hook between builds.
// DON'T
for (final id in courseIds) {
final state = useAutoComputedState(() => load(id)); // <- Hook count varies with the set
}
useMap solves this by holding a separate hook context per key. Each key's hooks always run together, and the set of keys can change between builds without disturbing the others.
Signature
Map<K, T> useMap<K extends Object, T>(Set<K> keys, T Function(K) block);
keys- the set of keys to run the block for. Keys are compared by==, so use value-equal keys (ids, enums) rather than throwaway instances.block- called once per key with that key, during build. It may call hooks. Its result is stored under that key in the returned map.
Use cases
- Loading or subscribing per item when the set of items changes at runtime - one
useAutoComputedStateper open course, per expanded row, per selected id. - Driving a dynamic number of per-item state hooks from a parent state hook, so the parent can aggregate or coordinate them (see the dynamic-N archetype in Custom hooks).
When the number of items is fixed and known at code time, you do not need useMap - just call the hook once per item. Reach for useMap only when keys are added or removed at runtime, as when the parent owns the key set as state:
// The parent owns the set of keys as state, so the loaded set can grow and
// shrink at runtime. The View reads each course's state by key.
class CourseList extends HookWidget {
const CourseList({super.key});
Widget build(BuildContext context) {
final openIds = useState<Set<String>>(const {});
final courseStates = useMap(
openIds.value,
(id) => useAutoComputedState(() => courseService.load(id), keys: [id]),
);
return Column(
children: [
for (final id in openIds.value)
Text(courseStates[id]?.valueOrNull?.id ?? 'Loading $id'),
TextButton(
onPressed: () => openIds.value = {...openIds.value, 'new'},
child: const Text('Open course'),
),
],
);
}
}
Caveats
- Per-key lifecycle on a key-set change: keys still present keep their hook state, newly added keys start fresh, and removed keys have their context disposed (on the post-build pass). The map identity stays stable across rebuilds, so a removed key drops its in-flight work and state.
- The block must call the same hooks for a given key on every build, exactly as in any hook context - the per-key relaxation is across different keys, not within one.
- Each key's
blockresult is rebuilt with that key's hooks; do not share a mutable object between keys expecting one instance.
See also
- useKeyed - the single-context primitive;
useMapis its per-key generalization - useIf - conditional hooks behind a boolean rather than a set of keys
- Custom hooks - per-item state archetypes and when
useMapis the right one