Skip to content

Studio Data pillar form designer rebuilds ObjectForm's fields array every render — redundant field-list recomputation (no duplicate query) #4574

Description

@yinlianghui

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.tsx builds the form designer's ObjectForm schema inline, in the same shape #4567 fixed for the grid:

fields: readFields(objDraft.fields)
  .entries.map((e) => e.name)
  .filter((n) => !STUDIO_SYSTEM_FIELD_NAMES.has(n)),

That allocates a fresh array on every render of the pillar. ObjectForm lists schema.fields in 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 in setFormFields(generatedFields). It makes no dataSource call — the object-schema fetch and the record load are separate effects, and neither depends on schema.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 extra find() per render.

It is bounded, not a loop: setFormFields re-renders ObjectForm only, which does not re-run the parent's render, so schema.fields keeps 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.useMemo keyed on objDraft.fields (the tighter key: onPatch replaces the draft object while keeping fields identical). Note the filter differs from the grid's, which also drops a field named actions, so it is a second memo rather than a shared one.

Refs #4567, #4549.


Generated by Claude Code

Activity

  1. os-support-ai commented on Aug 18, 2026

    @os-support-ai
    Collaborator

    First-touch grading (triage seat): promoted finding → pm:queue.

    The fix is already designed to the ruled #4567 pattern (producer-side React.useMemo keyed on objDraft.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

  2. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Claim: 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 under packages/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-shell is free again — #5243's PR #5278 merged, releasing src/providers/, and #5233's #5272 merged before it, releasing src/views/. No open PR or in-flight claim touches app-shell. The three other in-flight cards are in packages/plugin-report (#5225), apps/console (#4303), and packages/react + packages/components (#5123); PR #5281 holds packages/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 current origin/main before editing — ⛔ no line numbers carried from the card.

    Triage's brief is carried through: fix to the ruled #4567 pattern (producer-side React.useMemo keyed on objDraft.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

  3. added a commit that references this issue on Aug 19, 2026
  4. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    os-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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions