All files / hooks/use-merged-refs/src utils.ts

100% Statements 19/19
100% Branches 14/14
100% Functions 5/5
100% Lines 18/18

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                                  27x 10x   17x 13x                                               12x 11x 11x 19x 19x 2x   19x     11x 2x 2x 4x 4x 2x   2x              
import type { PossibleRef, RefCleanup } from "./types";
 
/**
 * Assign `value` to a single ref, whatever its form.
 *
 * - **Callback ref** — invoked with `value`. Its return value is passed through:
 *   React 19 callback refs may return a cleanup function.
 * - **Ref object** — its `.current` is set to `value`.
 * - **`null` / `undefined`** — ignored.
 *
 * @returns The callback ref's cleanup function if it returned one, otherwise
 * `void`.
 */
export function setRef<T>(
  ref: PossibleRef<T>,
  value: T
): RefCleanup | void {
  if (typeof ref === "function") {
    return ref(value) as RefCleanup | void;
  }
  if (ref !== null && ref !== undefined) {
    (ref as { current: T }).current = value;
  }
}
 
/**
 * Merge several refs into a single callback ref that forwards the node to all of
 * them. The returned callback is safe for both React 18 and 19:
 *
 * - It applies the node to every ref (callback or object), collecting any React
 *   19 cleanup functions.
 * - If **any** ref returned a cleanup, the merged callback returns a cleanup that
 *   runs each ref's cleanup (or resets object/callback refs to `null` for refs
 *   that did not return one). If none did, it returns `void`, so React 18 —
 *   which warns when a callback ref returns a value — stays happy.
 *
 * This is the non-hook core of {@link useMergedRefs}; use it when you need to
 * compose refs outside of render (e.g. in a class or a one-off).
 *
 * @param refs - The refs to merge. `null` / `undefined` entries are skipped.
 * @returns A callback ref that fans out to every provided ref.
 */
export function mergeRefs<T>(
  ...refs: PossibleRef<T>[]
): (node: T | null) => RefCleanup | void {
  return (node) => {
    let hasCleanup = false;
    const cleanups = refs.map((ref) => {
      const cleanup = setRef(ref, node as T);
      if (!hasCleanup && typeof cleanup === "function") {
        hasCleanup = true;
      }
      return cleanup;
    });
 
    if (hasCleanup) {
      return () => {
        for (let i = 0; i < cleanups.length; i++) {
          const cleanup = cleanups[i];
          if (typeof cleanup === "function") {
            cleanup();
          } else {
            setRef(refs[i], null as T);
          }
        }
      };
    }
  };
}