feat(hooks): introduce useUnmountCleanupRef for effect cleanup abstraction

Add useUnmountCleanupRef custom hook to centralize and simplify unmount
cleanup logic in document page components. Refactor EPUB, HTML, and PDF
page components to utilize this hook, replacing repetitive ref and effect
patterns for invoking cleanup functions on unmount. This reduces code
duplication and improves maintainability.
This commit is contained in:
Richard R 2026-05-27 16:22:14 -06:00
parent 139a227aa4
commit 3db193e012
4 changed files with 25 additions and 30 deletions

View file

@ -19,6 +19,7 @@ import { resolveDocumentId } from '@/lib/client/dexie';
import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { useFeatureFlag } from '@/contexts/RuntimeConfigContext';
import { useUnmountCleanupRef } from '@/hooks/useUnmountCleanupRef';
import { useEpubDocument } from './useEpubDocument';
export default function EPUBPage() {
@ -46,7 +47,6 @@ export default function EPUBPage() {
const inFlightDocIdRef = useRef<string | null>(null);
const loadedDocIdRef = useRef<string | null>(null);
const didInitPadPctRef = useRef(false);
const clearCurrDocRef = useRef(clearCurrDoc);
useEffect(() => {
setIsLoading(true);
@ -103,15 +103,7 @@ export default function EPUBPage() {
loadDocument();
}, [loadDocument, isLoading]);
useEffect(() => {
clearCurrDocRef.current = clearCurrDoc;
}, [clearCurrDoc]);
useEffect(() => {
return () => {
clearCurrDocRef.current();
};
}, []);
useUnmountCleanupRef(clearCurrDoc);
// Compute available height = viewport - (header height + tts bar height)
useEffect(() => {

View file

@ -17,6 +17,7 @@ import { RateLimitBanner } from '@/components/auth/RateLimitBanner';
import { AudiobookExportModal } from '@/components/AudiobookExportModal';
import { useAuthRateLimit } from '@/contexts/AuthRateLimitContext';
import { useFeatureFlag } from '@/contexts/RuntimeConfigContext';
import { useUnmountCleanupRef } from '@/hooks/useUnmountCleanupRef';
import type { TTSAudiobookChapter } from '@/types/tts';
import type { AudiobookGenerationSettings } from '@/types/client';
import { useHtmlDocument } from './useHtmlDocument';
@ -46,7 +47,6 @@ export default function HTMLPage() {
const [maxPadPx, setMaxPadPx] = useState<number>(0);
const inFlightDocIdRef = useRef<string | null>(null);
const loadedDocIdRef = useRef<string | null>(null);
const clearCurrDocRef = useRef(clearCurrDoc);
useEffect(() => {
setIsLoading(true);
@ -103,15 +103,7 @@ export default function HTMLPage() {
loadDocument();
}, [loadDocument, isLoading]);
useEffect(() => {
clearCurrDocRef.current = clearCurrDoc;
}, [clearCurrDoc]);
useEffect(() => {
return () => {
clearCurrDocRef.current();
};
}, []);
useUnmountCleanupRef(clearCurrDoc);
// Compute available height = viewport - (header height + tts bar height)
useEffect(() => {

View file

@ -26,6 +26,7 @@ import {
FORCE_REPARSE_CONFIRM_TITLE,
isForceReparseDisabled,
} from '@/lib/client/pdf/force-reparse';
import { useUnmountCleanupRef } from '@/hooks/useUnmountCleanupRef';
import { usePdfDocument } from './usePdfDocument';
// Dynamic import for client-side rendering only
@ -72,7 +73,6 @@ export default function PDFViewerPage() {
const [containerHeight, setContainerHeight] = useState<string>('auto');
const inFlightDocIdRef = useRef<string | null>(null);
const loadedDocIdRef = useRef<string | null>(null);
const clearCurrDocRef = useRef(clearCurrDoc);
const [isNavigatingBack, setIsNavigatingBack] = useState(false);
const parseUiState: NonNullable<typeof parseStatus> = parseStatus ?? 'pending';
const hasResolvedParseStatus = parseStatus !== null;
@ -153,15 +153,7 @@ export default function PDFViewerPage() {
loadDocument();
}, [loadDocument]);
useEffect(() => {
clearCurrDocRef.current = clearCurrDoc;
}, [clearCurrDoc]);
useEffect(() => {
return () => {
clearCurrDocRef.current();
};
}, []);
useUnmountCleanupRef(clearCurrDoc);
useEffect(() => {
if (isLoading) return;

View file

@ -0,0 +1,19 @@
import { useEffect, useRef } from 'react';
/**
* Runs cleanup only on unmount, while always invoking the latest cleanup function.
*/
export function useUnmountCleanupRef(cleanup: () => void) {
const cleanupRef = useRef(cleanup);
useEffect(() => {
cleanupRef.current = cleanup;
}, [cleanup]);
useEffect(() => {
return () => {
cleanupRef.current();
};
}, []);
}