fix react-hooks/exhaustive-deps warnings

This commit is contained in:
2026-05-01 04:42:36 +03:00
parent a332293e83
commit 8886abf589
9 changed files with 26 additions and 36 deletions
+2 -4
View File
@@ -32,16 +32,14 @@ export default function PersonalSection({ itemId, showRewatchToggle }: Props) {
// notes correctly without leaking state). // notes correctly without leaking state).
useEffect(() => { useEffect(() => {
setLocalNote(entry?.note || '') setLocalNote(entry?.note || '')
// eslint-disable-next-line react-hooks/exhaustive-deps }, [itemId, entry?.note])
}, [itemId])
// Debounce writes by 400ms. // Debounce writes by 400ms.
useEffect(() => { useEffect(() => {
if (localNote === (entry?.note || '')) return if (localNote === (entry?.note || '')) return
const id = setTimeout(() => setNote(itemId, localNote), 400) const id = setTimeout(() => setNote(itemId, localNote), 400)
return () => clearTimeout(id) return () => clearTimeout(id)
// eslint-disable-next-line react-hooks/exhaustive-deps }, [localNote, entry?.note, itemId, setNote])
}, [localNote])
const rating = entry?.rating ?? 0 const rating = entry?.rating ?? 0
const rewatchCount = entry?.rewatchCount ?? 0 const rewatchCount = entry?.rewatchCount ?? 0
+5 -6
View File
@@ -95,21 +95,21 @@ function RouletteModal({
const pick = pool[pickIndex % Math.max(1, pool.length)] || null const pick = pool[pickIndex % Math.max(1, pool.length)] || null
function spin() { const spin = useCallback(() => {
if (pool.length < 2) return if (pool.length < 2) return
let next = pickIndex let next = pickIndex
// Avoid landing on the same pick consecutively. // Avoid landing on the same pick consecutively.
while (next === pickIndex) next = Math.floor(Math.random() * pool.length) while (next === pickIndex) next = Math.floor(Math.random() * pool.length)
setPickIndex(next) setPickIndex(next)
setSpinNonce(n => n + 1) setSpinNonce(n => n + 1)
} }, [pool.length, pickIndex])
function open() { const open = useCallback(() => {
if (!pick) return if (!pick) return
const mediaType = pick.media_type === 'tv' || pick.first_air_date ? 'tv' : 'movie' const mediaType = pick.media_type === 'tv' || pick.first_air_date ? 'tv' : 'movie'
navigate(`/item/tmdb-${mediaType}-${pick.id}`) navigate(`/item/tmdb-${mediaType}-${pick.id}`)
onClose() onClose()
} }, [pick, navigate, onClose])
useEffect(() => { useEffect(() => {
function onKey(e: KeyboardEvent) { function onKey(e: KeyboardEvent) {
@@ -121,8 +121,7 @@ function RouletteModal({
} }
window.addEventListener('keydown', onKey) window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey)
// eslint-disable-next-line react-hooks/exhaustive-deps }, [pickIndex, pool.length, onClose, spin])
}, [pickIndex, pool.length])
const title = pick?.title || pick?.name || '' const title = pick?.title || pick?.name || ''
const year = (pick?.release_date || pick?.first_air_date || '').slice(0, 4) const year = (pick?.release_date || pick?.first_air_date || '').slice(0, 4)
+1 -2
View File
@@ -113,8 +113,7 @@ export default function RequestModal({ open, onClose, tmdbId, kind, tmdbData }:
const init: Record<number, boolean> = {} const init: Record<number, boolean> = {}
for (const s of tvSeasons) init[s.season_number ?? 0] = true for (const s of tvSeasons) init[s.season_number ?? 0] = true
setSeasonsRequested(init) setSeasonsRequested(init)
// eslint-disable-next-line react-hooks/exhaustive-deps }, [kind, tvSeasons])
}, [tmdbData?.id])
// Esc closes. // Esc closes.
useEffect(() => { useEffect(() => {
+1 -2
View File
@@ -133,6 +133,5 @@ export function usePlaybackReporting(args: Args) {
debugLog('[playback] stop report failed', err), debugLog('[playback] stop report failed', err),
) )
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [itemId, mediaSourceId, startTimeTicks, progressRef])
}, [itemId])
} }
+1 -2
View File
@@ -76,6 +76,5 @@ export function usePrebuffer(item: BaseItemDto | null | undefined, armed: boolea
}).catch(() => { /* warm-only; ignore */ }) }).catch(() => { /* warm-only; ignore */ })
return () => { cancelled = true } return () => { cancelled = true }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [armed, item?.Id, item?.Type, qc])
}, [armed, item?.Id])
} }
+1 -2
View File
@@ -92,8 +92,7 @@ export default function LibraryPage({ type }: Props) {
}, [selected.size, clearSelection]) }, [selected.size, clearSelection])
useEffect(() => { useEffect(() => {
return () => clearSelection() return () => clearSelection()
// eslint-disable-next-line react-hooks/exhaustive-deps }, [clearSelection])
}, [])
const { data: libraries } = useLibraries() const { data: libraries } = useLibraries()
const collectionType = COLLECTION_TYPE_MAP[type] const collectionType = COLLECTION_TYPE_MAP[type]
+13 -14
View File
@@ -332,18 +332,22 @@ export default function PlayerPage() {
/* Resume prompt: show on first mount when there's a saved position /* Resume prompt: show on first mount when there's a saved position
* past the threshold AND the user wants the prompt AND the URL didn't * past the threshold AND the user wants the prompt AND the URL didn't
* already specify ?resume=true (queue navigation path). */ * already specify ?resume=true (queue navigation path).
* Intentionally scoped to item?.Id only - we only want to evaluate
* the resume condition once per item, not re-trigger when item data
* refreshes or prefs change mid-playback. */
const resumePromptShownRef = useRef<string | null>(null)
useEffect(() => { useEffect(() => {
if (!item) return if (!item || resumePromptShownRef.current === item.Id) return
const pos = Number(item.UserData?.PlaybackPositionTicks ?? 0) const pos = Number(item.UserData?.PlaybackPositionTicks ?? 0)
const thresholdSec = usePreferencesStore.getState().resumeThresholdSec ?? 5 const thresholdSec = usePreferencesStore.getState().resumeThresholdSec ?? 5
const threshold = thresholdSec * 10_000_000 const threshold = thresholdSec * 10_000_000
const fromQueue = searchParams.get('resume') === 'true' const fromQueue = searchParams.get('resume') === 'true'
if (showResumePromptPref && !fromQueue && pos > threshold) { if (showResumePromptPref && !fromQueue && pos > threshold) {
setResumePromptOpen(true) setResumePromptOpen(true)
resumePromptShownRef.current = item.Id
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [item?.Id, searchParams, showResumePromptPref])
}, [item?.Id])
/* Auto-rewatch counter: when an already-played item starts playing /* Auto-rewatch counter: when an already-played item starts playing
* again, record the rewatch. We trip it at most once per item-mount * again, record the rewatch. We trip it at most once per item-mount
@@ -358,8 +362,7 @@ export default function PlayerPage() {
if (rewatchedItemIdRef.current === item.Id) return if (rewatchedItemIdRef.current === item.Id) return
rewatchedItemIdRef.current = item.Id rewatchedItemIdRef.current = item.Id
usePersonalData.getState().incrementRewatch(item.Id) usePersonalData.getState().incrementRewatch(item.Id)
// eslint-disable-next-line react-hooks/exhaustive-deps }, [item])
}, [item?.Id])
/* Auto-recap trigger: decide once per item. Recap card waits for the /* Auto-recap trigger: decide once per item. Recap card waits for the
* resume prompt (if any) to resolve before appearing. */ * resume prompt (if any) to resolve before appearing. */
@@ -376,8 +379,7 @@ export default function PlayerPage() {
} else { } else {
setRecapPending(false) setRecapPending(false)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [item?.Id, recapTrigger.shouldShow, showRecapCardPref, item])
}, [item?.Id, recapTrigger.shouldShow])
useEffect(() => { useEffect(() => {
if (recapPending && !resumePromptOpen) { if (recapPending && !resumePromptOpen) {
setRecapCardOpen(true) setRecapCardOpen(true)
@@ -429,8 +431,7 @@ export default function PlayerPage() {
qc.removeQueries({ queryKey: ['jellyfin', 'episodes', evictId], exact: false }) qc.removeQueries({ queryKey: ['jellyfin', 'episodes', evictId], exact: false })
} catch { /* ignore */ } } catch { /* ignore */ }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [id, qc])
}, [id])
/* Aggressive teardown on player unmount. /* Aggressive teardown on player unmount.
* *
@@ -638,8 +639,7 @@ export default function PlayerPage() {
p.currentTime = target p.currentTime = target
if (seriesId) recordSkippedSeconds(seriesId, 'credits', target - from) if (seriesId) recordSkippedSeconds(seriesId, 'credits', target - from)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [currentMarker?.type, currentMarker?.startSec, skipIntros, skipCredits, duration, seriesId])
}, [currentMarker?.type, currentMarker?.startSec, skipIntros, skipCredits])
/* Imperatively switch the active subtitle track. We use 'hidden' rather /* Imperatively switch the active subtitle track. We use 'hidden' rather
* than 'showing' so the browser doesn't paint its own caption UI over our * than 'showing' so the browser doesn't paint its own caption UI over our
@@ -720,8 +720,7 @@ export default function PlayerPage() {
// default: prefer a language match, then default-flagged, then off // default: prefer a language match, then default-flagged, then off
const match = pickSubtitle(subs, subtitleLanguage) const match = pickSubtitle(subs, subtitleLanguage)
setSubtitleIndex(match?.Index ?? null) setSubtitleIndex(match?.Index ?? null)
// eslint-disable-next-line react-hooks/exhaustive-deps }, [id, mediaSourceId, subtitleStreams.length, subtitleMode, subtitleLanguage, item])
}, [id, mediaSourceId, subtitleStreams.length, subtitleMode, subtitleLanguage])
/* ── Playback reporting ──────────────────────────────────── */ /* ── Playback reporting ──────────────────────────────────── */
const playSessionId = playbackInfo?.PlaySessionId || undefined const playSessionId = playbackInfo?.PlaySessionId || undefined
+1 -2
View File
@@ -110,8 +110,7 @@ export function SurpriseMeModal({
} else if (!open) { } else if (!open) {
setPick(null) setPick(null)
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [open, items])
}, [open, items.length])
return ( return (
<AnimatePresence> <AnimatePresence>
+1 -2
View File
@@ -117,8 +117,7 @@ export function ShortcutsSection() {
} }
window.addEventListener('keydown', onKey, true) window.addEventListener('keydown', onKey, true)
return () => window.removeEventListener('keydown', onKey, true) return () => window.removeEventListener('keydown', onKey, true)
// eslint-disable-next-line react-hooks/exhaustive-deps }, [capturingId, overrides, commit])
}, [capturingId, overrides])
return ( return (
<Section <Section