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:
parent
4f8f612d77
commit
6d910f504a
1 changed files with 56 additions and 52 deletions
|
|
@ -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 “{slug}” doesn’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 “{slug}” doesn’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 */}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue