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. 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;