All files / use-map/src useMap.ts

100% Statements 35/35
100% Branches 14/14
100% Functions 13/13
100% Lines 33/33

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133                          22x 22x                                                                                                                       41x 41x 22x     41x 22x         41x 41x   41x 10x   10x 1x   9x 9x 9x       41x 2x     41x 3x 3x 1x   2x 2x 2x       41x 3x     41x 2x     41x   41x 22x       41x    
import { useCallback, useMemo, useRef, useState } from "react";
import type {
  MapInitializer,
  UseMapActions,
  UseMapReturn,
} from "./types";
 
/**
 * Resolve a {@link MapInitializer} to a concrete `Map`. A function initializer
 * is invoked once; any iterable/Map is copied into a fresh `Map` so the caller's
 * original object is never mutated.
 */
function resolveInitial<K, V>(initial?: MapInitializer<K, V>): Map<K, V> {
  const value = typeof initial === "function" ? initial() : initial;
  return new Map(value ?? []);
}
 
/**
 * A React hook for managing `Map` state with immutable, ergonomic updates.
 *
 * Returns a tuple of the current (read-only) map and a stable set of actions.
 * Every mutation produces a brand-new `Map` so React re-renders correctly and
 * the previous state is never mutated in place. Updates that would not change
 * anything (removing an absent key, clearing an empty map, setting a key to the
 * value it already holds) are skipped to avoid needless re-renders.
 *
 * Features:
 * - Immutable updates (`new Map` on every change) with a `ReadonlyMap` return type
 * - Stable action identities — safe to use as effect dependencies
 * - `useState`-style lazy initialization; accepts `Map`, tuples, or a factory
 * - Full TypeScript generics for keys and values
 *
 * @template K - Key type.
 * @template V - Value type.
 * @param initialState - Initial entries, or a factory returning them. Defaults to empty.
 * @returns `[map, { set, setAll, remove, reset, clear, get }]`
 *
 * @example
 * ```tsx
 * function UserDirectory() {
 *   const [users, { set, remove }] = useMap<string, User>([
 *     ["1", { id: "1", name: "Alice" }],
 *     ["2", { id: "2", name: "Bob" }],
 *   ]);
 *
 *   return (
 *     <ul>
 *       {[...users.values()].map((u) => (
 *         <li key={u.id}>
 *           {u.name}
 *           <button onClick={() => remove(u.id)}>Remove</button>
 *         </li>
 *       ))}
 *       <button onClick={() => set("3", { id: "3", name: "Carol" })}>Add</button>
 *     </ul>
 *   );
 * }
 * ```
 *
 * @example
 * ```tsx
 * // Feature flags with reset-to-initial
 * const [flags, { set, reset }] = useMap<string, boolean>([
 *   ["darkMode", false],
 *   ["beta", true],
 * ]);
 * set("darkMode", true);
 * reset(); // back to the initial flags
 * ```
 */
export function useMap<K, V>(
  initialState?: MapInitializer<K, V>
): UseMapReturn<K, V> {
  // Resolve the initial map exactly once and keep it for `reset`.
  const initialRef = useRef<Map<K, V> | null>(null);
  if (initialRef.current === null) {
    initialRef.current = resolveInitial(initialState);
  }
 
  const [map, setMap] = useState<Map<K, V>>(
    () => new Map(initialRef.current as Map<K, V>)
  );
 
  // Mirror the latest map so `get` can be a stable callback that still reads
  // fresh state.
  const mapRef = useRef(map);
  mapRef.current = map;
 
  const set = useCallback((key: K, value: V) => {
    setMap((prev) => {
      // Skip re-render when the value is unchanged.
      if (prev.has(key) && Object.is(prev.get(key), value)) {
        return prev;
      }
      const next = new Map(prev);
      next.set(key, value);
      return next;
    });
  }, []);
 
  const setAll = useCallback((entries: Iterable<readonly [K, V]>) => {
    setMap(new Map(entries));
  }, []);
 
  const remove = useCallback((key: K) => {
    setMap((prev) => {
      if (!prev.has(key)) {
        return prev;
      }
      const next = new Map(prev);
      next.delete(key);
      return next;
    });
  }, []);
 
  const reset = useCallback(() => {
    setMap(new Map(initialRef.current as Map<K, V>));
  }, []);
 
  const clear = useCallback(() => {
    setMap((prev) => (prev.size === 0 ? prev : new Map<K, V>()));
  }, []);
 
  const get = useCallback((key: K) => mapRef.current.get(key), []);
 
  const actions = useMemo<UseMapActions<K, V>>(
    () => ({ set, setAll, remove, reset, clear, get }),
    [set, setAll, remove, reset, clear, get]
  );
 
  return [map, actions];
}