From 33adefd3aee224162718596daf68b575947ad552 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Mon, 6 Jul 2026 12:23:42 -0700 Subject: [PATCH 1/2] fix(plugin-detail): hoist DetailSection all-empty early return below hooks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DetailSection had an early `return null` (all-empty section, user hasn't revealed empties) placed BEFORE the virtual-scroll `useEffect`. When the same reconciled fiber rendered all-empty on one pass (N hooks, effect skipped) and populated on the next (N+1 hooks, effect runs), the hook count changed between renders and React threw error #300 ("rendered more hooks than during the previous render"). This is the master-detail drill-in crash: navigating from an account detail into a child project detail reuses the RecordDetailView / DetailSection fiber, and the sections flip from empty to populated — reliably tripping the mismatch. The orange error boundary appeared and a refresh bounced the user away. Fix: move the all-empty guard to AFTER every hook (including the useEffect) so the hook count is invariant to field/data/visibleCount. No behavior change for the render output. Verified in the showcase console: Accounts -> Northwind detail -> Projects tab -> Website Relaunch drill-in now renders with no crash; back-nav to the account and inline edit + validation + save all work. --- packages/plugin-detail/src/DetailSection.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/plugin-detail/src/DetailSection.tsx b/packages/plugin-detail/src/DetailSection.tsx index a7adf0c63a..5562b50639 100644 --- a/packages/plugin-detail/src/DetailSection.tsx +++ b/packages/plugin-detail/src/DetailSection.tsx @@ -179,9 +179,6 @@ export const DetailSection: React.FC = ({ ? section.fields.filter((field) => !isEmptyValue(field)) : section.fields; - // Hide entire section when all fields are empty AND user did not request to show them. - if (visibleFields.length === 0 && emptyCount === section.fields.length) return null; - // Apply auto-layout: infer columns and auto-span wide fields const { fields: layoutFields, columns: rawColumns } = applyDetailAutoLayout( visibleFields, @@ -450,6 +447,18 @@ export const DetailSection: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [vsEnabled, layoutFields.length, vsBatchSize]); + // Hide entire section when all fields are empty AND the user has not asked to + // reveal them. This early return MUST come AFTER every hook above (including + // the virtual-scroll useEffect) — never before. When a section is all-empty + // on one render (early return, N hooks) but has data on the next render (the + // useEffect runs, N+1 hooks) of the SAME reconciled fiber, the hook count + // changes between renders and React throws error #300 ("rendered more hooks + // than during the previous render"). This is the master-detail drill-in + // crash: navigating account → project reuses this DetailSection fiber, and + // its sections flip from empty to populated. Keeping the guard below all + // hooks makes the hook count invariant. + if (visibleFields.length === 0 && emptyCount === section.fields.length) return null; + const renderedFields = visibleCount !== undefined ? layoutFields.slice(0, visibleCount) : layoutFields; From 1d73d6b4bde7becf76f86223c1af85fb5b781cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8C=85=E5=91=A8=E6=B6=9B?= Date: Mon, 6 Jul 2026 12:24:16 -0700 Subject: [PATCH 2/2] chore(changeset): patch @object-ui/plugin-detail for #300 drill-in fix --- .changeset/detail-section-hooks-order.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/detail-section-hooks-order.md diff --git a/.changeset/detail-section-hooks-order.md b/.changeset/detail-section-hooks-order.md new file mode 100644 index 0000000000..31371599d6 --- /dev/null +++ b/.changeset/detail-section-hooks-order.md @@ -0,0 +1,7 @@ +--- +'@object-ui/plugin-detail': patch +--- + +Fix a React #300 crash when drilling from a master record into a related child record. + +`DetailSection` placed its all-empty `return null` guard *before* the virtual-scroll `useEffect`, so a section that rendered all-empty on one pass (effect skipped) and populated on the next (effect runs) changed its hook count between renders of the same reconciled fiber — React threw error #300 ("rendered more hooks than during the previous render"). This reliably tripped on the master-detail drill-in (e.g. Account → Project), showing an error boundary and bouncing the user away on refresh. The all-empty guard now runs after every hook, making the hook count invariant.