Skip to main content

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 useAutoComputedState per 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 block result 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; useMap is its per-key generalization
  • useIf - conditional hooks behind a boolean rather than a set of keys
  • Custom hooks - per-item state archetypes and when useMap is the right one