fix: stop pinned-library toggle from saving an empty list (#23)

The pin hook captured the optimistic ID list inside a React setState
updater. That updater can run after the following lines, so save ran with
optimistic=[] and overwrote server pins — matching the flash then wipe
when pinning a mounted Emby library after a local pin.

Compute previous/optimistic synchronously from a ref before calling setState.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: truewhile <truewhile@users.noreply.github.com>
This commit is contained in:
truewhile
2026-09-03 10:13:04 +08:00
committed by GitHub
parent b7f2760728
commit 936be24328
+20 -12
View File
@@ -12,6 +12,12 @@ export function usePinnedLibraries() {
const [syncing, setSyncing] = useState(false) const [syncing, setSyncing] = useState(false)
const [loadError, setLoadError] = useState(false) const [loadError, setLoadError] = useState(false)
const loadedRef = useRef(false) const loadedRef = useRef(false)
const pinnedIdsRef = useRef<string[]>([])
const syncingRef = useRef(false)
useEffect(() => {
pinnedIdsRef.current = pinnedIds
}, [pinnedIds])
useEffect(() => { useEffect(() => {
let cancelled = false let cancelled = false
@@ -22,6 +28,7 @@ export function usePinnedLibraries() {
.then((ids) => { .then((ids) => {
if (cancelled) return if (cancelled) return
loadedRef.current = true loadedRef.current = true
pinnedIdsRef.current = ids
setPinnedIds(ids) setPinnedIds(ids)
setLoadError(false) setLoadError(false)
}) })
@@ -29,7 +36,6 @@ export function usePinnedLibraries() {
if (cancelled) return if (cancelled) return
loadedRef.current = false loadedRef.current = false
setLoadError(true) setLoadError(true)
// Keep any existing pins in memory; never replace a known server list with [].
}) })
.finally(() => { .finally(() => {
if (!cancelled) setLoading(false) if (!cancelled) setLoading(false)
@@ -40,25 +46,27 @@ export function usePinnedLibraries() {
}, []) }, [])
const togglePin = useCallback(async (libraryId: string) => { const togglePin = useCallback(async (libraryId: string) => {
// Avoid saving from an unloaded/failed state — that would overwrite the if (!loadedRef.current || loading || loadError || syncingRef.current) return
// server list with only the clicked id and can wipe existing local pins
// when mounted Emby ids used to be filtered out server-side.
if (!loadedRef.current || loading || loadError) return
let previous: string[] = [] // Compute the next list synchronously from a ref. Do NOT capture the next
let optimistic: string[] = [] // value inside setState updater callbacks — React may defer those, leaving
setPinnedIds((current) => { // optimistic as [] and wiping the server-side pin list.
previous = current const previous = pinnedIdsRef.current
optimistic = togglePinnedLibraryId(current, libraryId) const optimistic = togglePinnedLibraryId(previous, libraryId)
return optimistic
}) pinnedIdsRef.current = optimistic
setPinnedIds(optimistic)
syncingRef.current = true
setSyncing(true) setSyncing(true)
try { try {
const saved = await savePinnedLibraryIds(optimistic) const saved = await savePinnedLibraryIds(optimistic)
pinnedIdsRef.current = saved
setPinnedIds(saved) setPinnedIds(saved)
} catch { } catch {
pinnedIdsRef.current = previous
setPinnedIds(previous) setPinnedIds(previous)
} finally { } finally {
syncingRef.current = false
setSyncing(false) setSyncing(false)
} }
}, [loading, loadError]) }, [loading, loadError])