Repository navigation
Studio Data pillar form designer rebuilds ObjectForm's fields array every render — redundant field-list recomputation (no duplicate query) #4574
Description
Activity
os-support-ai commented
on Aug 18, 2026 CollaboratorMore actionsFirst-touch grading (triage seat): promoted
finding→pm:queue.The fix is already designed to the ruled #4567 pattern (producer-side
React.useMemokeyed onobjDraft.fields, second memo — the filter set differs from the grid's). Promoting rather than shelving because the surface re-renders at keystroke rate and the card names a real latent hazard: any future data call in that effect's dependency chain turns this into a #4567 with no producer change. S mechanical; the card's "why this is NOT a #4567" section is the claimant's test brief — assert no new fetch appears, only identity stability.
Generated by Claude Code
os-support-ai commented
on Aug 19, 2026 CollaboratorMore actionsClaim: PM loop round 12 (refill)
Session:session_01RV6yuVCxymHYE16PL9vQkE
Branch:claude/issue-4574-form-designer-fields-memo
Worktree:objectui-issue-4574
Domain:repo:objectui(app-shell / studio-design)
File surface: the Studio Data pillar form designer underpackages/app-shell/src/views/studio-design/+ its tests (stop on breach; explain in the report)
Container & model: S,mode:subagent,model: sonnet(triage: "S mechanical")
Clause-②: no — identity stability only; no behaviour, no contract, no public surface.
Serial constraints cleared:packages/app-shellis free again — #5243's PR #5278 merged, releasingsrc/providers/, and #5233's #5272 merged before it, releasingsrc/views/. No open PR or in-flight claim touches app-shell. The three other in-flight cards are inpackages/plugin-report(#5225),apps/console(#4303), andpackages/react+packages/components(#5123); PR #5281 holdspackages/core. All disjoint.⚠️ Card age: filed 2026-08-13, six days ago, and app-shell has taken several merges today (#5272, #5278 among them). The dispatch order requires re-deriving the render path on currentorigin/mainbefore editing — ⛔ no line numbers carried from the card.Triage's brief is carried through: fix to the ruled #4567 pattern (producer-side
React.useMemokeyed onobjDraft.fields, a second memo because the filter set differs from the grid's), and the card's own "why this is NOT a #4567" section is the test brief — assert no new fetch appears, only identity stability. A test that proves a fetch was removed would be proving the wrong thing, since there was never a duplicate query here.
Generated by Claude Code
- added a commit that references this issue
on Aug 19, 2026 os-support-ai commented
on Aug 19, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 4574, "status": "done", "branch": "claude/issue-4574-form-designer-fields-memo", "pr": "https://github.com/objectstack-ai/objectui/pull/5285", "premise_still_valid": true, "summary": "Re-derived on current main (after #5272/#5278): DataPillar's form-preview still built ObjectForm's `fields` array inline on every render; confirmed ObjectForm still lists schema.fields by identity in its field-generation effect, and confirmed that effect is a pure derivation (no dataSource call) with the schema fetch and record load as separate, unrelated effects. Added a second React.useMemo (`formFields`), keyed on objDraft.fields, alongside the existing #4567 `gridColumns` memo -- not shared, since the form's filter omits the grid's `actions` exclusion. Replaced the inline array at the ObjectForm call site with the memo.", "tests": "New file packages/app-shell/src/views/studio-design/StudioDesignSurface.formFields.test.tsx: (1) three no-op re-renders leave every recorded schema.fields reference `toBe` the same object (Object.is identity, not toStrictEqual) since the settled mount, and dataSource find/findOne/getObjectSchema call counts stay flat -- asserts identity stability + no new fetch, per the card's brief, not fetch elimination (there was never a duplicate query on this path); (2) must-not-regress pin: a real '+ Add field' still produces a NEW fields identity (green in both pre-fix and post-fix states, mirroring gridColumns.test.tsx's own live-dependency pin). Reverse verification: committed fix+test (de68a2209), then `git checkout origin/main -- StudioDesignSurface.tsx` reverted only the source (test kept). Predicted before running: the identity-stability loop goes red (fresh inline array every render), fetch-count assertions stay green (never depended on schema.fields), test 2 stays green (invariant). Observed exactly that -- 1 failed/1 passed, failure at `expect(snap).toBe(stable)`: 'expected [ name, status ] to be [ name, status ] // Object.is equality' (content-equal, reference-different, confirming the assertion targets identity). Restored fix via `git checkout claude/issue-4574-form-designer-fields-memo -- <path>`; working tree clean against fix commit. No rebuild in this ablation's resolution path -- vitest imports the .tsx source directly for app-shell's own tests, no dist step to hide staleness. Final HEAD a4ba2ff85: `pnpm --filter '@object-ui/app-shell^...' build` clean; `pnpm --filter '@object-ui/app-shell' type-check` clean; `pnpm --filter '@object-ui/app-shell' build` clean; `pnpm --filter '@object-ui/app-shell' lint` 0 errors (2475 pre-existing warnings, none touching this change); `node scripts/check-control-bytes.mjs` OK; `node scripts/check-changeset-presence.mjs` OK (patch changeset added); `node scripts/check-changeset-no-major.mjs` OK; `pnpm exec vitest run packages/app-shell/src/views/studio-design/ --maxWorkers=2` -- 29 test files, 157 tests, all pass (run twice, before and after the changeset commit, both green). Narrowing declared: attempted the full `packages/app-shell/` suite (281 files) first but the shared /tmp/os-heavy-verify.lock was contended by other concurrent dev agents (confirmed via `fuser -v` -- held by another agent's flock/node process) long enough that it could not be observed to completion; per PM direction, narrowed to the studio-design/ directory (the pillar this PR touches) plus package-level build/type-check/lint, all completed in the foreground. packages/plugin-form/ (unmodified consumer) was not run locally -- out of narrowed scope. CI runs the full farm on the PR and is authoritative for the rest.", "open_questions": [], "out_of_scope_findings": [] }
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Observation-class finding, noticed while fixing #4567. Filed unassigned for triage; not the same severity as #4567 and deliberately not fixed there (out of that card's surface).
What it is
packages/app-shell/src/views/studio-design/StudioDesignSurface.tsxbuilds the form designer'sObjectFormschema inline, in the same shape #4567 fixed for the grid:That allocates a fresh array on every render of the pillar.
ObjectFormlistsschema.fieldsin an effect's dependency array by identity (packages/plugin-form/src/ObjectForm.tsx:695), so the effect re-runs on every render.Why this is NOT a #4567
I checked the effect body before filing (
ObjectForm.tsx:500-695): it is a pure field-list derivation ending insetFormFields(generatedFields). It makes nodataSourcecall — the object-schema fetch and the record load are separate effects, and neither depends onschema.fields.So the cost here is a redundant recomputation plus one extra state-set/render pass per pillar render — not a duplicate backend query. That is the whole difference from #4567, where the identical inline-array shape sat in
ListView's fetch dependency chain and issued a real extrafind()per render.It is bounded, not a loop:
setFormFieldsre-rendersObjectFormonly, which does not re-run the parent's render, soschema.fieldskeeps its identity within that cycle.Impact
No user-visible defect today. Wasted work on a surface that re-renders at keystroke rate, and a latent hazard: if anything in that effect's dependency chain ever grows a data call, this becomes a #4567 with no code change at the producer.
Suggested fix
Same producer-side memo as #4567 — hoist to
React.useMemokeyed onobjDraft.fields(the tighter key:onPatchreplaces the draft object while keepingfieldsidentical). Note the filter differs from the grid's, which also drops a field namedactions, so it is a second memo rather than a shared one.Refs #4567, #4549.
Generated by Claude Code