fix(reading-position): prevent spam saves during scroll animation

The issue was that scheduleSave and saveNow had syncEnabled/onSave in their
dependency arrays, causing them to be recreated when those props changed.
This triggered the scroll effect to unmount/remount repeatedly during smooth
scroll animations, flushing saves on each unmount.

Solution: Use refs (syncEnabledRef, onSaveRef) for all callback dependencies,
making scheduleSave and saveNow stable with empty dependency arrays. This
prevents effect re-runs and stops the save spam.

Now the scroll effect only runs once per article load, not on every render.
This commit is contained in:
Gigi
2025-10-23 00:19:04 +02:00
parent 8924f1b307
commit 30ae0d9dfb

View File

@@ -47,7 +47,7 @@ export const useReadingPosition = ({
// Debounced save function - simple 2s debounce
const scheduleSave = useCallback((currentPosition: number) => {
if (!syncEnabled || !onSave) {
if (!syncEnabledRef.current || !onSaveRef.current) {
return
}
@@ -61,7 +61,7 @@ export const useReadingPosition = ({
lastSavedPosition.current = 1
hasSavedOnce.current = true
lastSavedAtRef.current = Date.now()
onSave(1)
onSaveRef.current(1)
return
}
@@ -84,19 +84,21 @@ export const useReadingPosition = ({
lastSavedPosition.current = currentPosition
hasSavedOnce.current = true
lastSavedAtRef.current = Date.now()
onSave(currentPosition)
if (onSaveRef.current) {
onSaveRef.current(currentPosition)
}
saveTimerRef.current = null
}, DEBOUNCE_MS)
}, [syncEnabled, onSave])
}, [])
// Immediate save function
const saveNow = useCallback(() => {
if (!syncEnabled || !onSave) return
if (!syncEnabledRef.current || !onSaveRef.current) return
// Check suppression even for saveNow (e.g., during restore)
if (Date.now() < suppressUntilRef.current) {
const remainingMs = suppressUntilRef.current - Date.now()
console.log(`[reading-position] [${new Date().toISOString()}] ⏭️ saveNow() suppressed (${remainingMs}ms remaining) at ${Math.round(position * 100)}%`)
console.log(`[reading-position] [${new Date().toISOString()}] ⏭️ saveNow() suppressed (${remainingMs}ms remaining) at ${Math.round(positionRef.current * 100)}%`)
return
}
@@ -104,12 +106,12 @@ export const useReadingPosition = ({
clearTimeout(saveTimerRef.current)
saveTimerRef.current = null
}
console.log(`[reading-position] [${new Date().toISOString()}] 💾 saveNow() called at ${Math.round(position * 100)}%`)
lastSavedPosition.current = position
console.log(`[reading-position] [${new Date().toISOString()}] 💾 saveNow() called at ${Math.round(positionRef.current * 100)}%`)
lastSavedPosition.current = positionRef.current
hasSavedOnce.current = true
lastSavedAtRef.current = Date.now()
onSave(position)
}, [syncEnabled, onSave, position])
onSaveRef.current(positionRef.current)
}, [])
useEffect(() => {
if (!enabled) return