Origin: found while pinning objectui#6697 (the three census members outside #6592's dataConfig family). Reported here rather than folded into that PR, which is deliberately scoped to the three sites its card names.
The shape
usePermissions() (packages/permissions/src/usePermissions.ts) returns a useMemo:
returnuseMemo(()=>{if(!ctx){return{check: ...,can: ..., ... };}return{ ...ctx,can: ...,cannot: ... };},[ctx]);Both branches build a FRESH object on every call — an object literal with no provider mounted, a spread of ctx with one. Consumers then put the returned value straight into an effect dependency array. packages/plugin-list/src/ListView.tsx names it as perms in the data-fetch effect's deps, and the hook's own docblock names a second consumer:
Without this, downstream useMemo/useEffect deps see a fresh object each render and can enter infinite update loops (see DetailView gatedSchema -> data fetch effect, which would re-fire on every render otherwise).
useMemo carries no semantic guarantee: React may discard the cache and recompute even when [ctx] compares equal. On a discard the returned object gets a new identity while every permission it carries is unchanged, and the consuming fetch effect re-runs — an extra dataSource.find with nothing an author or a caller controls having changed. Same hazard as objectui#6018/#5976/#6591/#6592/#6697, different origin.
Why the #6592 census did not surface it
That sweep looked for a useEffect whose dependency array names a useMemo-bound identifier in the same file. Here the memo lives in packages/permissions and reaches the effect as an ordinary hook return, so the heuristic reads perms as a plain value. Any consumer that puts usePermissions() in a dependency array is in scope, not just ListView.
Measured, not inferred
While pinning objectui#6697 I had to keep the discard proxy from touching this memo so the ListView assertion could name expandFields and nothing else. Of that effect's ~19 dependencies, exactly two are memo-derived: expandFields (fixed under #6697) and perms (this card). The rest are props, useState values, or plain derived primitives.
Severity
Low, and the same shape as objectui#6697: the observable is a redundant round trip on a memo-cache discard, not incorrect data. The fix direction is the family's: key the effect on the primitives it actually reads off the permissions object, or give the hook a value-stable return. Note the docblock above — the memo also exists to stop an infinite loop, so it must not simply be removed.
Unassigned - filed as a finding for triage to route, not claimed.
Generated by Claude Code
Generated by Claude Code
Origin: found while pinning objectui#6697 (the three census members outside #6592's
dataConfigfamily). Reported here rather than folded into that PR, which is deliberately scoped to the three sites its card names.The shape
usePermissions()(packages/permissions/src/usePermissions.ts) returns auseMemo:Both branches build a FRESH object on every call — an object literal with no provider mounted, a spread of
ctxwith one. Consumers then put the returned value straight into an effect dependency array.packages/plugin-list/src/ListView.tsxnames it aspermsin the data-fetch effect's deps, and the hook's own docblock names a second consumer:useMemocarries no semantic guarantee: React may discard the cache and recompute even when[ctx]compares equal. On a discard the returned object gets a new identity while every permission it carries is unchanged, and the consuming fetch effect re-runs — an extradataSource.findwith nothing an author or a caller controls having changed. Same hazard as objectui#6018/#5976/#6591/#6592/#6697, different origin.Why the #6592 census did not surface it
That sweep looked for a
useEffectwhose dependency array names auseMemo-bound identifier in the same file. Here the memo lives inpackages/permissionsand reaches the effect as an ordinary hook return, so the heuristic readspermsas a plain value. Any consumer that putsusePermissions()in a dependency array is in scope, not justListView.Measured, not inferred
While pinning objectui#6697 I had to keep the discard proxy from touching this memo so the
ListViewassertion could nameexpandFieldsand nothing else. Of that effect's ~19 dependencies, exactly two are memo-derived:expandFields(fixed under #6697) andperms(this card). The rest are props,useStatevalues, or plain derived primitives.Severity
Low, and the same shape as objectui#6697: the observable is a redundant round trip on a memo-cache discard, not incorrect data. The fix direction is the family's: key the effect on the primitives it actually reads off the permissions object, or give the hook a value-stable return. Note the docblock above — the memo also exists to stop an infinite loop, so it must not simply be removed.
Unassigned - filed as a finding for triage to route, not claimed.
Generated by Claude Code
Generated by Claude Code