From 095e3944ae51aff390dfd62a6139e55f0c4bc656 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Tue, 28 Jul 2026 14:52:04 +0800 Subject: [PATCH] docs(client): state the reactive-read rules positively --- packages/client/AGENTS.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/client/AGENTS.md b/packages/client/AGENTS.md index 11be39a93d..a7d80b3232 100644 --- a/packages/client/AGENTS.md +++ b/packages/client/AGENTS.md @@ -18,14 +18,14 @@ The [slot system standard](../../.agents/notes/implemented/architecture/2026-07- ## Reactive read and contract-currency discipline -The three stale-UI bugs this section descends from shared one root: mutable state read during render without a subscription. The rules: +How live data reaches render code, and what may cross a business boundary: -1. **Everything a render reads that can change outside React arrives through a subscription**: a framework hook (rule 4 above), never a getter call, a `.getSnapshot()` in render, or a mirror copied into `useState`/a second store. Event handlers may read live snapshots (`keyboard.snapshot`); render may not. -2. **Business components contain no subscription machinery**: no `useSyncExternalStore`, no manual `useState`+`useEffect` subscribe pattern (it has a render-to-effect gap that drops notifications). A registrant-private reactive fact goes through the inject `hooks` compartment; a cross-entry fact goes through a store; a per-session fact goes through `sessions.provide`. -3. **Data-access ladder** — resolve needs in this order, and escalate rather than improvise: framework hooks (standing seats + provide/inject-bound `use`) → a declared store (`useStore`/`actions`) → inject callbacks → anything else is a new framework seam and needs main-thread arbitration, never a hand-rolled subscription. -4. **Contract currency is JSON-able data and callbacks.** Everything crossing a business boundary (owner props, inject faces, store state, provide contributions) is plain serializable data or a callback over such data; the inject `hooks` compartment is the one sanctioned carrier of bare observables, and components never see those either. ReactNode is NOT a currency: do not add new ReactNode-valued owner props or inject members (existing ones — composer `accessory`/`overlay`/`leftItems`/`rightItems` — are legacy under progressive removal; route new render content through a slot instead). -5. **An observable source keeps two identities stable**: the source object itself (hook binding is cached per source; a fresh source per render re-subscribes uSES), and its snapshot between changes (`getSnapshot` returns the same reference until the fact moves — a fresh object per call is an infinite re-render). -6. **Whoever rebuilds a published value republishes it through the same source in the same step.** Rebuild-without-notify is exactly the stale-roster bug; registration paths that can run after consumers exist must notify the live consumers (the slash late-source warm is the template). +1. **Everything a render reads that can change outside React arrives through a framework hook** (rule 4 above). Event-handler code may read live snapshots (e.g. `keyboard.snapshot`); render code subscribes. +2. **Business components contain no subscription machinery** — no `useSyncExternalStore`, no manual subscribe wiring, no mirroring an external snapshot into local state or a second store. Give each reactive fact its owning channel instead: registrant-private → the inject `hooks` compartment; cross-entry or remount-surviving → a declared store; per-session standard → `sessions.provide`. +3. **Data-access ladder** — resolve needs in this order: framework hooks (standing seats + provide/inject-bound `use`) → a declared store (`useStore`/`actions`) → inject callbacks → anything else is a new framework seam and needs main-thread arbitration. +4. **Contract currency is JSON-able data and callbacks.** Everything crossing a business boundary (owner props, inject faces, store state, provide contributions) is plain serializable data or a callback over such data; the inject `hooks` compartment is the one sanctioned carrier of bare observables, and components never see those either. ReactNode is not a currency: route render content through a slot; no new ReactNode-valued owner props or inject members (the composer's existing `accessory`/`overlay`/`leftItems`/`rightItems` seats are grandfathered and get migrated to slots progressively). +5. **An observable source keeps two identities stable**: the source object itself (hook binding is cached per source), and its snapshot between changes (`getSnapshot` returns the same reference until the fact moves). +6. **Whoever rebuilds a published value republishes it through the same source in the same step**, and a registration path that can run after consumers exist notifies the live consumers as part of registering. ## Export discipline (client plugin packages)