Cockpit version: 1.2.1 (scaffold v4.1.1)
Environment: Windows 11, Node v24, Chromium browser. Reproducible regardless of OS/browser (it is a client-side logic bug).
Summary
Opening any populated library item detail page (#/library/<lib>/<slug>) crashes the entire SPA to a blank white screen. Console shows:
Error: Minified React error #310 (Rendered more hooks than during the previous render)
at ... useMemo ...
at LibraryItemView (assets/LibraryView-*.js)
The bug is latent in the shipped product: the two bundled libraries (Recipes, Films & Series) ship empty, so the item detail view is never exercised with real content. It surfaces the moment any library has at least one item (I hit it with a community Business-Knowledge library that registers BKM/ via the standard library_registry mechanism).
Root cause
In web/src/views/LibraryView.tsx, component LibraryItemView, the metaRows useMemo is declared after the early returns:
function LibraryItemView({ lib, itemSlug }) {
const { data, loading, error } = useFetch(...);
const topRef = useRef(...);
useEffect(() => {...}, [lib, itemSlug]);
if (loading) return <ViewSkeleton />; // early return
if (error) return <div>...</div>; // early return
if (!data || !data.found || !data.item) { return ... } // early return
const item = data.item;
const metaRows = useMemo(() => {...}, [item]); // <-- hook AFTER conditional returns
...
}
On the first (loading) render an early return fires, so useMemo is skipped. On the next (data-loaded) render it runs — the hook count changes between renders, violating the Rules of Hooks → React #310 → unrecoverable render error → blank app.
Fix
Move the useMemo above all early returns so it runs on every render, computing from the possibly-undefined data?.item:
function LibraryItemView({ lib, itemSlug }) {
const { data, loading, error } = useFetch(...);
const topRef = useRef(...);
useEffect(() => {...}, [lib, itemSlug]);
const metaRows = useMemo<Array<[string, string]>>(() => {
const rows: Array<[string, string]> = [];
const it = data?.item;
if (!it) return rows;
for (const [k, v] of Object.entries(it)) {
if (k === 'body' || k === 'raw_frontmatter' || k === 'id' || k === 'slug' || k === 'file_path' || k === 'title') continue;
if (Array.isArray(v)) { if (v.length) rows.push([humanLabel(k), v.join(', ')]); continue; }
const s = scalarText(v as LibraryItem[string]);
if (s) rows.push([humanLabel(k), s]);
}
return rows;
}, [data]);
if (loading) return <ViewSkeleton />;
if (error) return <div>...</div>;
if (!data || !data.found || !data.item) { return ... }
const item = data.item;
// ... unchanged JSX that maps metaRows ...
}
Verified: with this change a fresh load of a populated library item renders the full detail page (body + Details panel) with zero console errors.
Suggested guards
- Add an error boundary around the route subtree so a single component error degrades to an inline message instead of blanking the whole app.
- Enable the
react-hooks/rules-of-hooks ESLint rule in CI — it catches this class of bug at build time.
Repro steps
- Register any library with ≥1 item (or add one recipe/film note so
recipes/movies is non-empty).
- Navigate to that library and click an item card.
- Observe the blank screen; console shows React #310 from
LibraryItemView.
Cockpit version: 1.2.1 (scaffold v4.1.1)
Environment: Windows 11, Node v24, Chromium browser. Reproducible regardless of OS/browser (it is a client-side logic bug).
Summary
Opening any populated library item detail page (
#/library/<lib>/<slug>) crashes the entire SPA to a blank white screen. Console shows:The bug is latent in the shipped product: the two bundled libraries (Recipes, Films & Series) ship empty, so the item detail view is never exercised with real content. It surfaces the moment any library has at least one item (I hit it with a community Business-Knowledge library that registers
BKM/via the standardlibrary_registrymechanism).Root cause
In
web/src/views/LibraryView.tsx, componentLibraryItemView, themetaRowsuseMemois declared after the early returns:On the first (loading) render an early return fires, so
useMemois skipped. On the next (data-loaded) render it runs — the hook count changes between renders, violating the Rules of Hooks → React #310 → unrecoverable render error → blank app.Fix
Move the
useMemoabove all early returns so it runs on every render, computing from the possibly-undefineddata?.item:Verified: with this change a fresh load of a populated library item renders the full detail page (body + Details panel) with zero console errors.
Suggested guards
react-hooks/rules-of-hooksESLint rule in CI — it catches this class of bug at build time.Repro steps
recipes/moviesis non-empty).LibraryItemView.