Skip to content

Instantly share code, notes, and snippets.

@markmals
Created May 30, 2025 02:03
Show Gist options
  • Save markmals/0000c36b1b880e053d5c1d7ea0ebe006 to your computer and use it in GitHub Desktop.
Save markmals/0000c36b1b880e053d5c1d7ea0ebe006 to your computer and use it in GitHub Desktop.
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
import { createSignal } from "solid-js";
import { createStore, reconcile } from "solid-js/store";
import { useSignal } from "./use-signal";
const [count, setCount] = createSignal(0);
const doubled = () => count() * 2;
export function Counter() {
const ct = useSignal(count);
const db = useSignal(doubled);
const increment = () => setCount(c => c + 1);
return (
<div>
<div>
{ct} × 2 = {db}
</div>
<button onClick={increment}>Increment</button>
</div>
);
}
const [counters, setCounters] = createStore([1, 2, 3, 4, 5]);
export function Counters() {
const prev = useSignal(() => counters);
const next = prev[prev.length - 1] ? prev[prev.length - 1] + 1 : 1;
const inc = () => setCounters(reconcile([...prev, next]));
const dec = () => setCounters(reconcile(prev.slice(0, -1)));
return (
<>
<h1>Counters</h1>
{prev ? prev.map(counter => <Counter key={counter} />) : <i>No Counters</i>}
<button onClick={inc}>Add More Counters</button>
<button onClick={dec}>Remove Counters</button>
</>
);
}
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<T>(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<T>(signal: Accessor<T>): 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<SignalCache>(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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment