Found while implementing objectui#6724 (usePermissions()'s returned identity). Filed rather than folded in: #6724's dispatch scope-locks that PR to the hook and its direct consumers.
The shape
Both permission providers build the context value in a useMemo:
packages/permissions/src/PermissionProvider.tsx line 124 — const value = useMemo(...), over four useCallbacks (check, checkField, getFieldPermissions, getRowFilter).packages/permissions/src/MePermissionsProvider.tsx line 285 — const value = useMemo(...), over six useCallbacks.
useMemo/useCallback carry no semantic guarantee: React may discard the cache and recompute even when the deps compare equal. The factory builds a fresh object every time, so a discard hands PermCtx.Provider a NEW context value while every permission it carries is unchanged. That re-renders every consumer of the context AND moves ctx identity — which is the key usePermissions() caches on after #6724, so the whole chain of consumer effects re-runs: ListView's data-fetch effect (an extra dataSource.find), DetailView's gatedSchema, ObjectForm/ModalForm/ObjectGrid/RelatedList.
Same hazard family as objectui#6018 / #5976 / #6591 / #6592 / #6697 / #6724, one link further up.
Why #6724 does not close it
#6724 made the hook's return stable while ctx is unchanged — it caches in a ref keyed on ctx identity, which a discard cannot move. That is the whole guarantee the hook can give: what it returns must reflect the ctx it was handed. The providers are the other end. With them fixed, the chain is discard-immune end to end; today it is immune at the hook only.
Measured, not inferred
On React 19.2.8 (this repo's pinned version) the cache is NOT discarded spontaneously — measured while fixing #6724: 51 re-renders with no provider, 51 with a provider and 42 under StrictMode each returned ONE identity, and there is no Activity/Offscreen subtree in this repo (the documented case where React does throw memo caches away). So this is a LATENT hazard, not a reproduction: a correctness dependency resting on a licence React has not yet exercised here. The forced-discard proxy in packages/permissions/src/__tests__/usePermissions.discardedIdentity.test.tsx is the technique that exercises it, and it is the technique a pin here would reuse.
Severity
Low, same as the rest of the family: the observable is a redundant round trip on a discard, not incorrect data.
Note that a value key is NOT available here either — what the context carries is verdict FUNCTIONS over an open set of object/field names, which flatten to no fixed primitive list. The route is the one #6724 took: cache the value where React cannot drop it, keyed on the inputs it is derived from.
Unassigned - filed as a finding for triage to route, not claimed.
Generated by Claude Code
Generated by Claude Code
Found while implementing objectui#6724 (
usePermissions()'s returned identity). Filed rather than folded in: #6724's dispatch scope-locks that PR to the hook and its direct consumers.The shape
Both permission providers build the context value in a
useMemo:packages/permissions/src/PermissionProvider.tsxline 124 —const value = useMemo(...), over fouruseCallbacks (check,checkField,getFieldPermissions,getRowFilter).packages/permissions/src/MePermissionsProvider.tsxline 285 —const value = useMemo(...), over sixuseCallbacks.useMemo/useCallbackcarry no semantic guarantee: React may discard the cache and recompute even when the deps compare equal. The factory builds a fresh object every time, so a discard handsPermCtx.Providera NEW context value while every permission it carries is unchanged. That re-renders every consumer of the context AND movesctxidentity — which is the keyusePermissions()caches on after #6724, so the whole chain of consumer effects re-runs:ListView's data-fetch effect (an extradataSource.find),DetailView's gatedSchema,ObjectForm/ModalForm/ObjectGrid/RelatedList.Same hazard family as objectui#6018 / #5976 / #6591 / #6592 / #6697 / #6724, one link further up.
Why #6724 does not close it
#6724 made the hook's return stable while
ctxis unchanged — it caches in a ref keyed onctxidentity, which a discard cannot move. That is the whole guarantee the hook can give: what it returns must reflect the ctx it was handed. The providers are the other end. With them fixed, the chain is discard-immune end to end; today it is immune at the hook only.Measured, not inferred
On React 19.2.8 (this repo's pinned version) the cache is NOT discarded spontaneously — measured while fixing #6724: 51 re-renders with no provider, 51 with a provider and 42 under
StrictModeeach returned ONE identity, and there is noActivity/Offscreen subtree in this repo (the documented case where React does throw memo caches away). So this is a LATENT hazard, not a reproduction: a correctness dependency resting on a licence React has not yet exercised here. The forced-discard proxy inpackages/permissions/src/__tests__/usePermissions.discardedIdentity.test.tsxis the technique that exercises it, and it is the technique a pin here would reuse.Severity
Low, same as the rest of the family: the observable is a redundant round trip on a discard, not incorrect data.
Note that a value key is NOT available here either — what the context carries is verdict FUNCTIONS over an open set of object/field names, which flatten to no fixed primitive list. The route is the one #6724 took: cache the value where React cannot drop it, keyed on the inputs it is derived from.
Unassigned - filed as a finding for triage to route, not claimed.
Generated by Claude Code
Generated by Claude Code