From 829ec4bf6e78edf3e6754b04338bb9dd2b442451 Mon Sep 17 00:00:00 2001 From: Gigi Date: Thu, 23 Oct 2025 00:20:55 +0200 Subject: [PATCH] fix(reading-position): fix infinite loop causing spam saves The root cause was scheduleSave being in the scroll effect's dependency array. Even though scheduleSave had an empty dependency array, React still saw it as a dependency and re-ran the effect constantly, causing unmount/remount loops and triggering flush-on-unmount repeatedly. Solution: Store scheduleSave in a ref (scheduleSaveRef) and call it via the ref in the scroll handler. This removes scheduleSave from the effect dependencies while still allowing the scroll handler to access the latest version. This fixes the "Maximum update depth exceeded" error and stops the spam saves. --- src/hooks/useReadingPosition.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/hooks/useReadingPosition.ts b/src/hooks/useReadingPosition.ts index 1e230a3c..7a9aca92 100644 --- a/src/hooks/useReadingPosition.ts +++ b/src/hooks/useReadingPosition.ts @@ -31,6 +31,7 @@ export const useReadingPosition = ({ const suppressUntilRef = useRef(0) const syncEnabledRef = useRef(syncEnabled) const onSaveRef = useRef(onSave) + const scheduleSaveRef = useRef<((pos: number) => void) | null>(null) // Keep refs in sync with props useEffect(() => { @@ -91,6 +92,11 @@ export const useReadingPosition = ({ }, DEBOUNCE_MS) }, []) + // Store scheduleSave in ref for use in scroll handler + useEffect(() => { + scheduleSaveRef.current = scheduleSave + }, [scheduleSave]) + // Immediate save function const saveNow = useCallback(() => { if (!syncEnabledRef.current || !onSaveRef.current) return @@ -143,7 +149,7 @@ export const useReadingPosition = ({ // Schedule auto-save if sync is enabled (unless suppressed) if (Date.now() >= suppressUntilRef.current) { - scheduleSave(clampedProgress) + scheduleSaveRef.current?.(clampedProgress) } else { const remainingMs = suppressUntilRef.current - Date.now() console.log(`[reading-position] [${new Date().toISOString()}] 🛡️ Save suppressed (${remainingMs}ms remaining) at ${Math.round(clampedProgress * 100)}%`) @@ -207,7 +213,7 @@ export const useReadingPosition = ({ clearTimeout(completionTimerRef.current) } } - }, [enabled, onPositionChange, onReadingComplete, readingCompleteThreshold, scheduleSave, completionHoldMs]) + }, [enabled, onPositionChange, onReadingComplete, readingCompleteThreshold, completionHoldMs]) // Reset reading complete state when enabled changes useEffect(() => {