All files / hooks/use-event-callback/src useEventCallback.ts

100% Statements 7/7
50% Branches 1/2
100% Functions 4/4
100% Lines 6/6

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          1x                                                                       9x 2x     9x 7x     9x    
import { useCallback, useEffect, useLayoutEffect, useRef } from "react";
 
// SSR-safe layout effect: keep the stored callback in sync before paint on the
// client, fall back to useEffect on the server.
const useIsomorphicLayoutEffect =
  typeof window !== "undefined" ? useLayoutEffect : useEffect;
 
/**
 * Returns a stable, memoized callback that always invokes the latest version of
 * `fn` — the community equivalent of React's experimental `useEffectEvent`.
 *
 * The returned function keeps the same identity for the component's lifetime,
 * so it is safe to pass to `useEffect` deps, event listeners, or memoized
 * children, while still "seeing" the newest props and state when called.
 *
 * @param fn - The callback whose latest version should be invoked
 * @returns A referentially-stable function that proxies to the latest `fn`
 *
 * @example
 * ```tsx
 * const [count, setCount] = useState(0);
 * const onClick = useEventCallback(() => console.log(count));
 * // `onClick` never changes identity, but always logs the current count
 * useEffect(() => {
 *   el.addEventListener("click", onClick);
 *   return () => el.removeEventListener("click", onClick);
 * }, [onClick]);
 * ```
 *
 * @remarks
 * Intended for callbacks fired **after** render/paint (events, timeouts,
 * subscriptions). The stored function is updated in a layout effect, so:
 * - Calling the returned function **during render throws** (guards against
 *   reading state that isn't committed yet).
 * - Do not call it from an effect that commits *before* this component's layout
 *   effect on the same pass (e.g. a child component's layout effect) — it may
 *   still hold the previous render's function.
 */
export function useEventCallback<Args extends unknown[], Return>(
  fn: (...args: Args) => Return
): (...args: Args) => Return {
  const ref = useRef<(...args: Args) => Return>(() => {
    throw new Error("useEventCallback: cannot call the callback while rendering.");
  });
 
  useIsomorphicLayoutEffect(() => {
    ref.current = fn;
  }, [fn]);
 
  return useCallback((...args: Args) => ref.current(...args), []);
}