From 936be243289b72b4b1c16e74909422d16415d3a7 Mon Sep 17 00:00:00 2001 From: truewhile <62226914+truewhile@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:13:04 +0800 Subject: [PATCH] fix: stop pinned-library toggle from saving an empty list (#23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Co-authored-by: truewhile --- web/src/hooks/usePinnedLibraries.ts | 32 ++++++++++++++++++----------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/web/src/hooks/usePinnedLibraries.ts b/web/src/hooks/usePinnedLibraries.ts index 858d1a6..523824f 100644 --- a/web/src/hooks/usePinnedLibraries.ts +++ b/web/src/hooks/usePinnedLibraries.ts @@ -12,6 +12,12 @@ export function usePinnedLibraries() { const [syncing, setSyncing] = useState(false) const [loadError, setLoadError] = useState(false) const loadedRef = useRef(false) + const pinnedIdsRef = useRef([]) + const syncingRef = useRef(false) + + useEffect(() => { + pinnedIdsRef.current = pinnedIds + }, [pinnedIds]) useEffect(() => { let cancelled = false @@ -22,6 +28,7 @@ export function usePinnedLibraries() { .then((ids) => { if (cancelled) return loadedRef.current = true + pinnedIdsRef.current = ids setPinnedIds(ids) setLoadError(false) }) @@ -29,7 +36,6 @@ export function usePinnedLibraries() { if (cancelled) return loadedRef.current = false setLoadError(true) - // Keep any existing pins in memory; never replace a known server list with []. }) .finally(() => { if (!cancelled) setLoading(false) @@ -40,25 +46,27 @@ export function usePinnedLibraries() { }, []) const togglePin = useCallback(async (libraryId: string) => { - // Avoid saving from an unloaded/failed state — that would overwrite the - // 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 + if (!loadedRef.current || loading || loadError || syncingRef.current) return - let previous: string[] = [] - let optimistic: string[] = [] - setPinnedIds((current) => { - previous = current - optimistic = togglePinnedLibraryId(current, libraryId) - return optimistic - }) + // Compute the next list synchronously from a ref. Do NOT capture the next + // value inside setState updater callbacks — React may defer those, leaving + // optimistic as [] and wiping the server-side pin list. + const previous = pinnedIdsRef.current + const optimistic = togglePinnedLibraryId(previous, libraryId) + + pinnedIdsRef.current = optimistic + setPinnedIds(optimistic) + syncingRef.current = true setSyncing(true) try { const saved = await savePinnedLibraryIds(optimistic) + pinnedIdsRef.current = saved setPinnedIds(saved) } catch { + pinnedIdsRef.current = previous setPinnedIds(previous) } finally { + syncingRef.current = false setSyncing(false) } }, [loading, loadError])