useMap
EditManages an immutable Map with stable update controls.
import { useCallback, useEffect, useLayoutEffect, useRef, useState,} from "react";
export type MapEntries<Key, Value> = | ReadonlyMap<Key, Value> | readonly (readonly [Key, Value])[];
export interface UseMapResult<Key, Value> { readonly map: ReadonlyMap<Key, Value>; readonly set: (key: Key, value: Value) => void; readonly setMany: (entries: Iterable<readonly [Key, Value]>) => void; readonly update: ( key: Key, updater: ( this: void, value: Value | undefined, exists: boolean, ) => Value, ) => void; readonly remove: (key: Key) => void; readonly clear: () => void; readonly reset: () => void;}
const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
export function useMap<Key, Value>( initialEntries: MapEntries<Key, Value> = [],): UseMapResult<Key, Value> { const resetSnapshot = new Map(initialEntries); const [map, setMap] = useState(() => resetSnapshot); const initialRef = useRef<ReadonlyMap<Key, Value>>(resetSnapshot);
useIsomorphicLayoutEffect(() => { initialRef.current = resetSnapshot; });
const set = useCallback((key: Key, value: Value) => { setMap((current) => { if (current.has(key) && Object.is(current.get(key), value)) return current; const next = new Map(current); next.set(key, value); return next; }); }, []);
const setMany = useCallback( (entries: Iterable<readonly [Key, Value]>) => { const materialized = Array.from(entries); if (materialized.length === 0) return;
setMap((current) => { const next = new Map(current); for (const [key, value] of materialized) { next.set(key, value); }
for (const [key] of materialized) { if ( current.has(key) !== next.has(key) || !Object.is(current.get(key), next.get(key)) ) { return next; } } return current; }); }, [], );
const update = useCallback( ( key: Key, updater: ( this: void, value: Value | undefined, exists: boolean, ) => Value, ) => { setMap((current) => { const exists = current.has(key); const value = updater(current.get(key), exists); if (exists && Object.is(current.get(key), value)) { return current; } const next = new Map(current); next.set(key, value); return next; }); }, [], );
const remove = useCallback((key: Key) => { setMap((current) => { if (!current.has(key)) return current; const next = new Map(current); next.delete(key); return next; }); }, []);
const clear = useCallback(() => { setMap((current) => (current.size === 0 ? current : new Map())); }, []);
const reset = useCallback(() => { setMap(new Map(initialRef.current)); }, []);
return { map, set, setMany, update, remove, clear, reset };}Download
Section titled “Download”wget -O src/hooks/useMap.ts https://raw.githubusercontent.com/jrTilak/lazykit/HEAD/registry/react-hooks/useMap.tstemp_dir="$(mktemp -d)" && bunx degit jrTilak/lazykit/registry/react-hooks "$temp_dir" && mv "$temp_dir/useMap.ts" src/hooks/useMap.ts && rm -r "$temp_dir"Examples
Section titled “Examples”import { useMap } from "./useMap";
export function MapExample() { const scores = useMap<string, number>([["Ada", 1]]);
return ( <div> <ul> {Array.from(scores.map, ([name, score]) => ( <li key={name}>{name}: {score}</li> ))} </ul> <button type="button" onClick={() => scores.update("Ada", (score) => (score ?? 0) + 1)} > Add point </button> <button type="button" onClick={() => scores.set("Grace", 1)}> Add Grace </button> </div> );}initialEntries(ReadonlyMap<Key, Value> | readonly [Key, Value][], default:[]) — Initial entries and latest reset snapshot.
Returns
Section titled “Returns”UseMapResult<Key, Value> with readonly map, set, setMany, update,
remove, clear, and reset. setMany materializes an iterable once before
the React state update, so one-shot generators are safe. The update
callback receives both the current value and an exists flag, distinguishing
a missing key from a key whose value is undefined.