import { useCallback, useRef, useSyncExternalStore } from "react"; import type { Accessor } from "solid-js"; import { observable } from "solid-js"; import { unwrap } from "solid-js/store"; /** * Creates a shallow clone of the input value. * - For arrays, return a new array with the same elements. * - For objects, return a new object with the same properties. * - For primitives or null, returns the value as-is. */ function shallowClone(v: T): T { if (typeof v !== "object" || v === null) return v; return Array.isArray(v) ? ([...v] as unknown as T) : ({ ...v } as T); } /** * React hook to subscribe to a Solid signal (or reactive function) and * expose its value to React components, with proper reactivity and * referential stability for React's rendering model. * * @param signal - A Solid `Accessor` (signal or derived computation) * @returns The current value of the signal, updated reactively. */ export function useSignal(signal: Accessor): T { const version = useRef(0); /** * Subscribes to the reactive function and all of its nested properties (if object/array). */ const subscribe = useCallback( (invalidate: () => void) => { const $signal = observable(() => { const raw = signal(); if (typeof raw === "object" && raw !== null) { if (Array.isArray(raw)) { // Access each array element to track all indices. raw.forEach((_, i) => raw[i]); } else { // Access each property to track all keys. for (const k in raw) raw[k as keyof typeof raw]; } } return raw; }); let first = true; const { unsubscribe } = $signal.subscribe(() => { // The first call happens immediately on mount, so we skip it. if (first) { first = false; return; } version.current++; invalidate(); }); return unsubscribe; }, [signal], ); /** * Returns a stable snapshot of the signal's value. * - If the value is primitive, return it directly. * - If the value is an object/array, return a shallow clone, * cached per version, so React's Object.is equality check works * as expected. */ type SignalCache = { value: T; version: number } | null; const cache = useRef(null); const getSnapshot = useCallback((): T => { const raw = signal(); if (typeof raw !== "object" || raw === null) return raw; // If the version hasn't changed, return the cached value. if (cache.current?.version === version.current) { return cache.current.value; } // Otherwise, create a new shallow clone and cache it. const snap = shallowClone(unwrap(raw)); cache.current = { value: snap, version: version.current }; return snap; }, [signal]); return useSyncExternalStore(subscribe, getSnapshot); }