fix: stable preview nav handlers via ref
react-hotkeys-hook can fire a stale closure when the callback dependency array changes between renders, causing arrow nav to read an old photos array (e.g. the empty initial render before visiblePhotoIds was applied) and land on the wrong photo or no-op entirely. Move the latest photos / activePhotoId into a navRef updated on every render. The goPrev / goNext callbacks become stable (their useCallback deps shrink to just setActivePhoto) and read the freshest values from the ref at fire time. useHotkeys no longer has to re-bind on every render — the handlers can capture the ref once. The visible-order array still drives navigation; this just removes the re-bind race that was making it look like nav was ignoring it. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,22 +45,38 @@ export function PreviewView() {
|
|||||||
const safeIndex = currentIndex < 0 ? 0 : currentIndex
|
const safeIndex = currentIndex < 0 ? 0 : currentIndex
|
||||||
const currentPhoto: Photo | undefined = photos[safeIndex]
|
const currentPhoto: Photo | undefined = photos[safeIndex]
|
||||||
|
|
||||||
|
// Keep the latest photos array + active id in a ref so the keyboard
|
||||||
|
// handlers ALWAYS read the freshest state. Without this, react-hotkeys-
|
||||||
|
// hook can fire a closure that captured an older photos array (e.g.
|
||||||
|
// the empty initial render before visiblePhotoIds was applied) and
|
||||||
|
// arrow nav lands on the wrong photo or no-ops.
|
||||||
|
const navRef = useRef({ photos, activePhotoId })
|
||||||
|
navRef.current = { photos, activePhotoId }
|
||||||
|
|
||||||
const goPrev = useCallback(() => {
|
const goPrev = useCallback(() => {
|
||||||
if (photos.length === 0) return
|
const { photos: ps, activePhotoId: aid } = navRef.current
|
||||||
const next = Math.max(0, safeIndex - 1)
|
if (ps.length === 0) return
|
||||||
setActivePhoto(photos[next].id)
|
const idx = aid ? ps.findIndex((p) => p.id === aid) : 0
|
||||||
}, [photos, safeIndex, setActivePhoto])
|
const safe = idx < 0 ? 0 : idx
|
||||||
|
const next = Math.max(0, safe - 1)
|
||||||
|
setActivePhoto(ps[next].id)
|
||||||
|
}, [setActivePhoto])
|
||||||
|
|
||||||
const goNext = useCallback(() => {
|
const goNext = useCallback(() => {
|
||||||
if (photos.length === 0) return
|
const { photos: ps, activePhotoId: aid } = navRef.current
|
||||||
const next = Math.min(photos.length - 1, safeIndex + 1)
|
if (ps.length === 0) return
|
||||||
setActivePhoto(photos[next].id)
|
const idx = aid ? ps.findIndex((p) => p.id === aid) : 0
|
||||||
}, [photos, safeIndex, setActivePhoto])
|
const safe = idx < 0 ? 0 : idx
|
||||||
|
const next = Math.min(ps.length - 1, safe + 1)
|
||||||
|
setActivePhoto(ps[next].id)
|
||||||
|
}, [setActivePhoto])
|
||||||
|
|
||||||
// Preview-scoped hotkeys: only mounted while PreviewView is rendered.
|
// Preview-scoped hotkeys: only mounted while PreviewView is rendered.
|
||||||
|
// The handlers themselves are stable (refs internally) so the deps
|
||||||
|
// array stays empty — useHotkeys won't have to re-bind on every render.
|
||||||
useHotkeys('escape', closePreview, { preventDefault: true })
|
useHotkeys('escape', closePreview, { preventDefault: true })
|
||||||
useHotkeys('left', goPrev, { preventDefault: true }, [goPrev])
|
useHotkeys('left', goPrev, { preventDefault: true })
|
||||||
useHotkeys('right', goNext, { preventDefault: true }, [goNext])
|
useHotkeys('right', goNext, { preventDefault: true })
|
||||||
useHotkeys('i', () => setInfoPanelOpen((v) => !v), { preventDefault: true })
|
useHotkeys('i', () => setInfoPanelOpen((v) => !v), { preventDefault: true })
|
||||||
|
|
||||||
// Preload the immediate neighbors so arrow nav feels instant. Skip videos
|
// Preload the immediate neighbors so arrow nav feels instant. Skip videos
|
||||||
|
|||||||
Reference in New Issue
Block a user