/**
* src/book/Chapter.tsx
*
* Route component for /book/:id — lazy-loads the MDX module for the chapter,
* renders it inside the MDXProvider with all book components injected globally.
*/
import { Suspense, lazy, useEffect, useRef, type ComponentType } from 'react';
import { useParams, Navigate } from 'react-router-dom';
import { MDXProvider } from '@mdx-js/react';
import { CHAPTERS, getChapterById, getPrevNext } from './lib/chapters.ts';
import ChapterNav from './layout/ChapterNav.tsx';
import * as BookComponents from './components/index.ts';
// Build the lazy component map at module initialisation time (not during render)
// so the same component object is reused across navigations.
const LAZY_MAP = new Map(
CHAPTERS.map((ch) => [ch.id, lazy(ch.load as () => Promise<{ default: ComponentType }>)]),
);
function ChapterFallback() {
return (
Loading…
);
}
export default function Chapter() {
const { id } = useParams<{ id: string }>();
const scrolledRef = useRef(null);
const chapter = id ? getChapterById(id) : undefined;
const { prev, next } = id ? getPrevNext(id) : { prev: null, next: null };
// Scroll to top when chapter changes
useEffect(() => {
if (id && scrolledRef.current !== id) {
scrolledRef.current = id;
window.scrollTo({ top: 0, behavior: 'instant' });
}
}, [id]);
if (!chapter) {
return ;
}
// The lazy component is pre-built in LAZY_MAP at module initialisation (above),
// not created during this render — the linter can't trace through Map.get().
const LazyContent = LAZY_MAP.get(chapter.id)!;
return (
{/* Chapter header */}
{chapter.part}
{chapter.title}
{chapter.subtitle && (
{chapter.subtitle}
)}
{/* MDX content with all book components available globally */}
}>
{/* The lazy component referencing LAZY_MAP is stable (module-level map),
not created during this render. The linter cannot trace through Map.get(). */}
{/* eslint-disable-next-line react-hooks/static-components -- module-level lazy */}
{/* Prev / Next navigation */}
);
}