All files / src/react useValue.ts

100% Statements 22/22
100% Branches 11/11
100% Functions 3/3
100% Lines 22/22

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 791x             1x                                           1x   2x   1x   1x                                     1x 62x 40x 40x 40x   62x   62x   62x 62x 62x     62x 62x   62x   62x   62x 62x  
import {
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  type Readable,
  type ReadableLike,
  type Unwrap,
  getReadable,
} from "@embra/reactivity";
import { useDebugValue, useMemo, useSyncExternalStore } from "react";
 
export interface UseValue {
  /**
   * Accepts a {@link ReadableLike} and returns the latest value.
   * It only triggers re-rendering when new value emitted from $ (base on {@link Readable.$version} instead of React's `Object.is` comparison).
   *
   * @param $ A {@link ReadableLike}.
   * @returns the value of the {@link ReadableLike}
   */
  <T = any>($: ReadableLike<T>): T;
 
  /**
   * Accepts a {@link ReadableLike} and returns the latest value.
   * It only triggers re-rendering when new value emitted from $ (base on {@link Readable.$version} instead of React's `Object.is` comparison).
   *
   * @param $ A {@link ReadableLike}.
   * @returns the value of the {@link ReadableLike}, or $ itself if $ is not a {@link ReadableLike}
   */
  <T = any, U = any>($: ReadableLike<T> | U): T | U;
}
 
const noop = () => {
  /* noop */
};
 
const returnsNoop = () => noop;
 
const defaultArgs = [returnsNoop, returnsNoop as () => any] as const;
 
/**
 * Accepts a {@link ReadableLike} and returns the latest value.
 * It only triggers re-rendering when new value emitted from $ (base on {@link Readable.$version} instead of React's `Object.is` comparison).
 *
 * @param $ A {@link ReadableLike}.
 * @returns the value of the {@link ReadableLike}, or $ itself if $ is not a {@link ReadableLike}
 *
 * @example
 * ```tsx
 * import { useValue } from "@embra/reactivity/react";
 *
 * function App({ count$ }) {
 *   const count = useValue(count$);
 *   return <div>{count}</div>;
 * }
 * ```
 */
export const useValue: UseValue = <T, U>($?: ReadableLike<T> | U): Unwrap<T> | U => {
  const args = useMemo(() => {
    const readable = getReadable($);
    return (
      readable && ([(onChange: () => void) => readable.subscribe(onChange), () => readable.$version, readable] as const)
    );
  }, [$]);
 
  const [subscriber, getSnapshot, readable] = args ?? defaultArgs;
 
  const version = useSyncExternalStore(
    subscriber,
    getSnapshot,
    // It is safe to use the same value getter for server snapshot since val() can
    // be initialized with a default value.
    getSnapshot,
  );
 
  const value = useMemo(() => (readable ? readable.get() : $), [version, readable, $]);
 
  useDebugValue(value);
 
  return value;
};