docs(core): correct the useStoreSnapshot caller contract
Copilot review round 2. The contract claimed an unstable `read`/`whenAbsent` trips React's "The result of getSnapshot should be cached" error. It does not. Measured on React 19: an inline `read` closure throws nothing, logs nothing, and forces no extra render pass. React's dev check calls getSnapshot twice within a single render, where the closure is unchanged, so the cache answers both calls identically and the warning never fires. The real cost is quiet — a fresh value every render, defeating downstream useMemo/React.memo/effect deps — which is a better reason to state the contract than an error that never arrives. Also stop requiring module scope for every fallback. A primitive `whenAbsent` (`false`, `undefined`, `"disconnected"`) is stable under Object.is and is passed inline throughout these hooks, so the rule contradicted the code it was describing. Only object/array fallbacks need a constant, which is what NO_TOOLS and NO_PAGINATION are. Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
f6105773f0
commit
8f3619f618
1 changed files with 22 additions and 14 deletions
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
|
@ -56,7 +56,7 @@ const CardContent = Group.withProps({
|
|||
- The `onChange` runs **during render**, so it must be pure — `setState` and nothing else. No fetches, DOM writes, logging, ref mutation, or parent callbacks; a render can be replayed or abandoned.
|
||||
- Effects remain correct for real external-system synchronization (DOM measurement, rAF, subscriptions, timers).
|
||||
- **Subscribing to an `@inspector/core` state store is `useSyncExternalStore`, never `useState` + a subscribing `useEffect`** — that shape re-seeds state from the store prop, so it carries the same stale frame plus a window where an event dispatched between render and subscribe is lost. `useValueChange` is web-only and `core/react/` is shared with the CLI and TUI.
|
||||
- Use **`useStoreSnapshot(store, event, read, whenAbsent)`** (`core/react/useStoreSnapshot.ts`), once per value, when the getter returns a **fresh value per read** (a defensive copy, or a freshly built object) — nearly all of them. When the snapshot is **already referentially stable**, subscribe directly and skip the helper, as `useListError` does with its `Error | null`. Pass `read`/`whenAbsent` as **module-scope constants**: they are part of the cache key, so an unstable one trips React's "getSnapshot should be cached" error.
|
||||
- Use **`useStoreSnapshot(store, event, read, whenAbsent)`** (`core/react/useStoreSnapshot.ts`), once per value, when the getter returns a **fresh value per read** (a defensive copy, or a freshly built object) — nearly all of them. When the snapshot is **already referentially stable**, subscribe directly and skip the helper, as `useListError` does with its `Error | null`. `read`/`whenAbsent` must be **referentially stable** — they are part of the cache key. `read` is a function, so module scope; `whenAbsent` needs a module-scope constant only when it is an object or array, since a primitive fallback (`false`, `undefined`, a string) is stable already and is passed inline in these hooks. An unstable one fails **quietly** — no throw, no React warning, no extra render, just a fresh value each render that defeats downstream memoization.
|
||||
- The snapshot is cached against the store's per-event dispatch counter (`TypedEventTarget.getEventRevision`), **not** its contents — a contents compare cannot see a dispatch that mutated an entry already in the list. Don't replace it with a shallow compare.
|
||||
- The store is the source of truth; the event is only the signal. A test firing a store event must set the value on the store first, and a fake store must extend the real `TypedEventTarget` (a bare `EventTarget` lacks `getEventRevision`).
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue