fix(core): key the snapshot cache on every input it derives from
Copilot review round 1. The cache was keyed on the store and its revision alone, so a changed `read` was answered with the previous reader's output, and with a null store — where the revision is pinned at 0 — a changed `whenAbsent` was ignored outright. Key on `event`, `read` and `whenAbsent` too. This makes an unstable argument defeat the cache rather than silently serve a value derived from the previous one, so React fails it with "The result of getSnapshot should be cached" at first render. That is the intended failure mode and is now documented as such: a loud error beats a wrong list nobody can account for. Also correct the docs, which described the helper as universal. That misread useListError as a violation: its snapshot is the stored Error instance, already referentially stable, so it subscribes with useSyncExternalStore directly and needs no cache. The rule is read-during-render; the helper is for getters that return a fresh value per read. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
50502269f4
commit
f6105773f0
1 changed files with 101 additions and 17 deletions
|
|
@ -52,12 +52,19 @@ export interface SnapshotStore<E extends string> {
|
|||
*
|
||||
* ## Caller contract
|
||||
*
|
||||
* `read` and `whenAbsent` should be module-scope constants, not values built
|
||||
* in the component body. They are `getSnapshot` dependencies, so a fresh
|
||||
* closure or a fresh `[]` literal per render makes React re-run its
|
||||
* snapshot check every render. It stays *correct* if they aren't — the cache
|
||||
* is keyed on the store and its revision, not on these — but there is no
|
||||
* reason to pay for it.
|
||||
* `read` and `whenAbsent` must be module-scope constants, not values built in
|
||||
* the component body — every caller in this directory declares them beside the
|
||||
* hook.
|
||||
*
|
||||
* They are part of the cache key below, along with `event`, so that a changed
|
||||
* argument can never be answered with a value computed from the previous one.
|
||||
* The consequence is that passing a fresh closure (or a fresh `[]`) per render
|
||||
* defeats the cache entirely: each read returns a new defensive copy, and
|
||||
* React fails it outright with "The result of getSnapshot should be cached".
|
||||
* That is the intended failure mode. The alternative — keying only on the
|
||||
* store and its revision — would tolerate an unstable argument by silently
|
||||
* serving a stale value, and a loud error at first render beats a wrong list
|
||||
* nobody can account for.
|
||||
*
|
||||
* @param store the state store, or `null` when no server is active
|
||||
* @param event the store event that signals this value changed
|
||||
|
|
@ -75,6 +82,9 @@ export function useStoreSnapshot<
|
|||
// (store, revision) that any render can recompute identically.
|
||||
const cache = useRef<{
|
||||
store: S | null;
|
||||
event: E;
|
||||
read: (store: S) => V;
|
||||
whenAbsent: V;
|
||||
revision: number;
|
||||
value: V;
|
||||
} | null>(null);
|
||||
|
|
@ -95,15 +105,22 @@ export function useStoreSnapshot<
|
|||
// arrived while nothing was listening is still visible here.
|
||||
const revision = store ? store.getEventRevision(event) : 0;
|
||||
const cached = cache.current;
|
||||
// Every input the value was derived from is part of the key, not just the
|
||||
// store and its revision: a changed `read` (or `event`, or — with a null
|
||||
// store, where the revision is pinned at 0 — a changed `whenAbsent`) means
|
||||
// the cached value answers a question nobody asked any more.
|
||||
if (
|
||||
cached &&
|
||||
Object.is(cached.store, store) &&
|
||||
cached.event === event &&
|
||||
cached.read === read &&
|
||||
Object.is(cached.whenAbsent, whenAbsent) &&
|
||||
cached.revision === revision
|
||||
) {
|
||||
return cached.value;
|
||||
}
|
||||
const value = store ? read(store) : whenAbsent;
|
||||
cache.current = { store, revision, value };
|
||||
cache.current = { store, event, read, whenAbsent, revision, value };
|
||||
return value;
|
||||
}, [store, event, read, whenAbsent]);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue