fix: move scroll-spy hooks above early returns to fix React hooks ordering crash

M016 added useState/useMemo/useEffect hooks after conditional early
returns (loading/notFound/error), violating React rules of hooks.
Moved all hooks above the early returns so they execute on every render.
This commit is contained in:
jlightner 2026-04-03 06:46:16 +00:00
parent 4f8f612d77
commit 6d910f504a

View file

@ -172,63 +172,22 @@ export default function TechniquePage() {
};
}, [slug, selectedVersion]);
// Scroll to hash fragment after technique loads (key moments, section anchors, etc.)
useEffect(() => {
if (!technique) return;
const hash = window.location.hash.slice(1);
if (hash) {
const el = document.getElementById(hash);
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
}, [technique]);
if (loading) {
return <div className="loading">Loading technique</div>;
}
if (notFound) {
return (
<div className="technique-404">
<h2>Technique Not Found</h2>
<p>The technique &ldquo;{slug}&rdquo; doesn&rsquo;t exist.</p>
<Link to="/" className="btn">
Back to Home
</Link>
</div>
);
}
if (error || !technique) {
return (
<div className="loading error-text">
Error: {error ?? "Unknown error"}
</div>
);
}
// Overlay snapshot fields when viewing a historical version
const isHistorical = selectedVersion !== "current" && versionDetail != null;
const overlay = isHistorical
? snapshotToOverlay(versionDetail.content_snapshot)
: null;
const displayTitle = overlay?.title ?? technique.title;
const displaySummary = overlay?.summary ?? technique.summary;
const displayCategory = overlay?.topic_category ?? technique.topic_category;
const displayTags = overlay?.topic_tags ?? technique.topic_tags;
const displaySections = overlay?.body_sections ?? technique.body_sections;
const displayFormat = overlay?.body_sections_format ?? technique.body_sections_format ?? "v1";
const displayChains = overlay?.signal_chains ?? technique.signal_chains;
const displayPlugins = overlay?.plugins ?? technique.plugins;
const displayQuality = overlay?.source_quality ?? technique.source_quality;
// --- Scroll-spy: activeId for ToC and ReadingHeader ---
// IMPORTANT: These hooks must be above early returns to maintain
// consistent hook ordering across renders (React rules of hooks).
const [activeId, setActiveId] = useState<string>("");
const [h1Visible, setH1Visible] = useState(true);
const h1Ref = useRef<HTMLHeadingElement>(null);
// Overlay snapshot fields when viewing a historical version
const isHistorical = selectedVersion !== "current" && versionDetail != null;
const overlay = (isHistorical && technique)
? snapshotToOverlay(versionDetail.content_snapshot)
: null;
const displaySections = technique ? (overlay?.body_sections ?? technique.body_sections) : null;
const displayFormat = technique ? (overlay?.body_sections_format ?? technique.body_sections_format ?? "v1") : "v1";
// Build flat list of all section/subsection IDs for observation
const allSectionIds = useMemo(() => {
if (displayFormat !== "v2" || !Array.isArray(displaySections)) return [];
@ -292,8 +251,53 @@ export default function TechniquePage() {
return () => observer.disconnect();
}, [allSectionIds]);
// Scroll to hash fragment after technique loads
useEffect(() => {
if (!technique) return;
const hash = window.location.hash.slice(1);
if (hash) {
const el = document.getElementById(hash);
if (el) {
el.scrollIntoView({ behavior: "smooth", block: "start" });
}
}
}, [technique]);
const currentSectionHeading = sectionHeadingMap.get(activeId) ?? "";
if (loading) {
return <div className="loading">Loading technique</div>;
}
if (notFound) {
return (
<div className="technique-404">
<h2>Technique Not Found</h2>
<p>The technique &ldquo;{slug}&rdquo; doesn&rsquo;t exist.</p>
<Link to="/" className="btn">
Back to Home
</Link>
</div>
);
}
if (error || !technique) {
return (
<div className="loading error-text">
Error: {error ?? "Unknown error"}
</div>
);
}
// Re-derive display fields now that we know technique is non-null
const displayTitle = overlay?.title ?? technique.title;
const displaySummary = overlay?.summary ?? technique.summary;
const displayCategory = overlay?.topic_category ?? technique.topic_category;
const displayTags = overlay?.topic_tags ?? technique.topic_tags;
const displayChains = overlay?.signal_chains ?? technique.signal_chains;
const displayPlugins = overlay?.plugins ?? technique.plugins;
const displayQuality = overlay?.source_quality ?? technique.source_quality;
return (
<article className="technique-page">
{/* Reading header — v2 pages only */}