Skip to content

census(finding): both permission providers build their context value in a useMemo, so a discard churns ctx identity for every usePermissions() consumer #6813

Description

@claude

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

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions