Skip to content

Commit bde0780

Browse files
claude[bot]claude
andauthored
docs(agents): rule out depending on a useMemo/useCallback identity (#9257)
Eight separately-filed instances were one defect shape: code resting on the identity of a memoised value or callback, which React documents as a performance hint whose cache it may discard. Write the rule once in section 5 next to the other React authoring commandments, state the value-side / effect-side seam the family measured, and declare in the rule text that nothing enforces it — a lint rule reaches the same-file shape only. Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK Co-authored-by: Claude <noreply@anthropic.com>
1 parent b2bb813 commit bde0780

2 files changed

Lines changed: 8 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
---
3+
4+
Write down the rule the `useMemo` / `useCallback` identity family has been
5+
asking for across eight instances (objectui#8640): objectui code may not
6+
depend on the identity of a `useMemo` / `useCallback` result. `AGENTS.md`
7+
only; no package is released by this change.

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ interface BaseSchema {
107107
- **#7 — No-Touch zones (Shadcn purity).** `packages/components/src/ui/**/*.tsx` are upstream 3rd-party files overwritten by sync scripts — **never edit their logic/styles**. To change `Button`/`Dialog` behavior: create/edit a wrapper in `packages/components/src/custom/`, import the primitive from `@/ui/...`, and wrap it.
108108
- **#8 — UI state lives where it can survive (objectui#2269, ADR-0054 C3).** Classify every piece of UI state before writing it: **addressable** (user would share it / expect it back after refresh / Back should respect it) → the **URL** (`?recordId=`, `?form=`, `?tab=` — constants in `app-shell/src/urlParams.ts`, never string literals); **preference** (stable across records/sessions) → localStorage or server prefs; **truly ephemeral** (hover, open dropdown — nobody cares if it's lost) → component state. The rule: **state that must survive a data refresh may never live only in an uncontrolled component** (that's how the detail-tab reset happened — objectui#2257). Corollary: **refresh data, don't rebuild UI** — after a save/action, invalidate the affected data (`notifyDataChanged` from `@object-ui/react`) so consumers refetch in place; never bump a `key=` to remount a subtree (it destroys scroll, collapsed sections, tab state, in-progress inline edits, and triggers a refetch storm).
109109
- **#9 — A "verified" claim is only as good as the check that re-verifies it (objectui#7833).** Prose that declares something checked, counted, aligned or covered — in a doc, a code comment, a script or workflow header, or this file — states a fact that was derived once, when it was written, and is derived never again; no later reading can tell such an assertion from a live one. So when you declare something verified, **point at the instrument that re-derives it — the script, the gate, the test — and never write down its answer**: "the count this gate prints", not the count; "the list this script enumerates", not the list. A figure that is still correct is the dangerous case, not the safe one — a reader who spot-checks it confirms it and is still wrong once the population it described has moved out from under it. Where nothing re-derives the claim, say that in the text rather than let it read as live. This file is not exempt: it has carried such a claim and needed a repair (objectui#7800). The measured instances behind this rule sit on objectui#7833 — pointed at, not copied here, which is this rule applied to itself.
110+
- **#10 — Never depend on the IDENTITY of a `useMemo` / `useCallback` result (objectui#8640, ruled over eight instances: objectui#5976 · #6018 · #6591 · #6592 · #6697 · #6724 · #6813 · #6862).** React documents both hooks as a **performance hint** and reserves the right to throw the cache away and recompute even when the dependency list compares equal — the identity they hand back is something the library never promised. Memoise for cost freely; just never let correctness rest on the object or function that comes back — ⛔ not as a `useEffect` / `useMemo` dependency, ⛔ not as a `Map` / `WeakMap` key, ⛔ not in a reference comparison. Re-key the consumer on the **primitives or the payload objects it actually reads** (objectui#6592), or move the cache out of React onto those same inputs (`packages/permissions/src/discardProofCache.ts`, objectui#6813). **Both sides of the seam, or neither holds:** a value-side cache keyed on a payload object is walked straight through by an effect that still keys on a memoised identity — a discard re-runs the fetch and `setData(json)` installs a **fresh payload object even for a byte-identical answer**, so every such cache misses and a new value reaches every consumer (objectui#6813 defeated by #6862, measured on objectui#8640). ⇒ a provider that refetches may not republish an equal payload as a new object, and a consumer that needs stability keys on the **data**, not on a cache. ⚠️ **Nothing enforces this rule** — said here per #9 rather than left to be assumed: a lint rule is feasible for the same-file shape only (`eslint-rules/` would host it), while the instances that crossed a module boundary — a prop, a hook return, a context — are out of reach of any single-file rule; the feasibility reading and its counts are on objectui#8640's PR. ⚠️ Nor does an ordinary test see the defect: React does not discard on its own in this tree, so a pin that does not **force** a discard passes identically on defect and fix — `providerCtxIdentity.discarded.test.tsx` in `packages/permissions` forces one.
110111

111112
---
112113

0 commit comments

Comments
 (0)