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>
63 lines
1.7 KiB
TypeScript
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;
|
|
};
|
|
}
|