Files
WRNexusJS/packages/react/src/snapshot-cache.ts
T
ClintchizandClaude Opus 5 468f63f378 feat(react): add referentially-stable snapshot and selector caches
useSyncExternalStore requires getSnapshot to return an identical
reference when unchanged; @wrnexus/store's readonlySnapshot returns a
fresh clone per call. The cache lives here rather than in the store
package so existing consumers are untouched.

createSelectorCache takes an optional equality function: the Object.is
default can never stabilize a selector that allocates, which is the
usual source of infinite re-renders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:11:22 +05:30

63 lines
1.7 KiB
TypeScript

export interface SnapshotSource<S extends object> {
snapshot(): Readonly<S>;
subscribe(listener: () => void): () => void;
}
export interface SnapshotCache<S extends object> {
getSnapshot(): Readonly<S>;
dispose(): void;
}
/**
* Wraps a store instance so repeated `getSnapshot()` calls return the same
* reference until the store notifies a change. `useSyncExternalStore` throws
* and spins if given a fresh object each call, which `@wrnexus/store`'s
* `readonlySnapshot` does by design.
*/
export function createSnapshotCache<S extends object>(source: SnapshotSource<S>): SnapshotCache<S> {
let cached: Readonly<S> | undefined;
let dirty = true;
const unsubscribe = source.subscribe(() => {
dirty = true;
});
return {
getSnapshot() {
if (dirty || cached === undefined) {
cached = source.snapshot();
dirty = false;
}
return cached;
},
dispose() {
unsubscribe();
},
};
}
/**
* Memoizes a selector over a cached snapshot. Without this, any mutation
* re-renders every island bound to the store, because snapshots are whole-state.
*/
export function createSelectorCache<S extends object, R>(
getSnapshot: () => Readonly<S>,
selector: (state: Readonly<S>) => R,
isEqual: (a: R, b: R) => boolean = Object.is,
): () => R {
let lastSnapshot: Readonly<S> | undefined;
let lastResult: R;
let initialized = false;
return () => {
const snapshot = getSnapshot();
if (!initialized || snapshot !== lastSnapshot) {
const next = selector(snapshot);
if (!initialized || !isEqual(next, lastResult)) lastResult = next;
lastSnapshot = snapshot;
initialized = true;
}
return lastResult;
};
}