Observed while fixing #4618 (PR #4623), which traced the same expression shape one file over. Filed unassigned; out of that card's scope — #4618 is the STATIC table path, this is the provider: 'object' sibling. Duplicate-searched (keyword + ObjectDataTable / finalData path): nothing open. The closest relatives are both closed — #4567 (a columns-array identity in a fetch dependency chain) and #4618 itself.
What
packages/plugin-dashboard/src/ObjectDataTable.tsx:245
const finalData = Array.isArray(rawData) ? rawData : [];
The non-array fallback is a fresh array literal per render, and finalData is a dependency of the derivedColumns memo (:332). So while rawData is anything other than an array — the whole pre-fetch window, and any render where the provider hands back a non-array — the memo's key changes on every render and derivedColumns recomputes: buildFieldMeta per column, a fresh cell closure per column, the isSystemField pass, the fieldLabel lookups. All of it discarded, because the memo returns [] at :320 (if (finalData.length === 0) return []) before any of it is used.
ESLint already reports it and has all along:
warning The 'finalData' conditional could make the dependencies of useMemo Hook (at line 332)
change on every render. To fix this, wrap the initialization of 'finalData' in its own
useMemo() Hook react-hooks/exhaustive-deps
Why this is observation-class, not a defect a user hits
Named explicitly so triage does not have to re-derive it: this cannot produce #4618's render loop. That loop needed the churn to feed a setState, and it needed the state write to re-render the component that regenerates the literal. Here the churn feeds a memo only. It reaches data-table's columns prop, but data-table re-rendering does not re-render ObjectDataTable, so nothing sustains it — and since PR #4623 the columns sync compares by value, so the churn no longer costs even the one extra child render it used to.
What is left is wasted work in the loading window, and one live ESLint warning that documents a real identity bug. Nothing renders wrong.
Fix shape
The same one-line move PR #4623 made in data-table.tsx: a module-scope frozen empty instead of a per-render literal, so "no rows yet" is a stable value. ObjectDataTable.tsx:245 is the only site; schema.data and boundData are already stable when present.
Worth pairing with a sweep for the same expression across the package — Array.isArray(x) ? x : [] inside a component body is the shape, and it is only ever safe when nothing memoizes or synchronizes on the result.
Generated by Claude Code
Observed while fixing #4618 (PR #4623), which traced the same expression shape one file over. Filed unassigned; out of that card's scope — #4618 is the STATIC
tablepath, this is theprovider: 'object'sibling. Duplicate-searched (keyword +ObjectDataTable/finalDatapath): nothing open. The closest relatives are both closed — #4567 (a columns-array identity in a fetch dependency chain) and #4618 itself.What
packages/plugin-dashboard/src/ObjectDataTable.tsx:245The non-array fallback is a fresh array literal per render, and
finalDatais a dependency of thederivedColumnsmemo (:332). So whilerawDatais anything other than an array — the whole pre-fetch window, and any render where the provider hands back a non-array — the memo's key changes on every render andderivedColumnsrecomputes:buildFieldMetaper column, a freshcellclosure per column, theisSystemFieldpass, thefieldLabellookups. All of it discarded, because the memo returns[]at:320(if (finalData.length === 0) return []) before any of it is used.ESLint already reports it and has all along:
Why this is observation-class, not a defect a user hits
Named explicitly so triage does not have to re-derive it: this cannot produce #4618's render loop. That loop needed the churn to feed a
setState, and it needed the state write to re-render the component that regenerates the literal. Here the churn feeds a memo only. It reachesdata-table'scolumnsprop, butdata-tablere-rendering does not re-renderObjectDataTable, so nothing sustains it — and since PR #4623 the columns sync compares by value, so the churn no longer costs even the one extra child render it used to.What is left is wasted work in the loading window, and one live ESLint warning that documents a real identity bug. Nothing renders wrong.
Fix shape
The same one-line move PR #4623 made in
data-table.tsx: a module-scope frozen empty instead of a per-render literal, so "no rows yet" is a stable value.ObjectDataTable.tsx:245is the only site;schema.dataandboundDataare already stable when present.Worth pairing with a sweep for the same expression across the package —
Array.isArray(x) ? x : []inside a component body is the shape, and it is only ever safe when nothing memoizes or synchronizes on the result.Generated by Claude Code