Skip to content

census(finding): usePermissions()'s memoised return is consumed by effect-dependency identity, outside objectui#6592's census heuristic #6724

Description

@claude

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

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