From 5cc177ccf90b1b1229a6a96c9b44e7545f9bc92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Thu, 9 Apr 2026 21:14:36 +0200 Subject: [PATCH] fix(dashboard): prevent stale DB data from overwriting newer localStorage layout Two related fixes: 1. When localStorage has a saved layout, mark it as authoritative so a DB response carrying older data (e.g. from a save cancelled by navigating away within the 2-second debounce window) cannot overwrite it. 2. Flush any pending debounced DB save immediately on component unmount so that navigating away within the window doesn't silently lose changes. Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/hooks/useDashboardLayout.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/apps/web/src/hooks/useDashboardLayout.ts b/apps/web/src/hooks/useDashboardLayout.ts index 217d568..b82f086 100644 --- a/apps/web/src/hooks/useDashboardLayout.ts +++ b/apps/web/src/hooks/useDashboardLayout.ts @@ -69,6 +69,8 @@ export function useDashboardLayout() { const hasHydratedFromStorageRef = useRef(false); // Holds the DB config that needs to be persisted to localStorage once userId is available. const pendingStorageSaveRef = useRef | null>(null); + // Holds the latest layout that has been queued for DB save but not yet flushed. + const pendingLayoutSaveRef = useRef(null); // Once userId is known, hydrate from user-scoped localStorage (if no DB data yet). useEffect(() => { @@ -79,6 +81,10 @@ export function useDashboardLayout() { hasHydratedFromStorageRef.current = true; if (stored) { setConfig(stored); + // Treat localStorage data as authoritative local state so a stale DB + // response (e.g. from a save that was cancelled by page navigation within + // the 2-second debounce window) cannot overwrite it. + hasLocalChangesBeforeHydrationRef.current = true; } // localStorage check is complete — show whatever we have now. setIsHydrated(true); @@ -146,10 +152,21 @@ export function useDashboardLayout() { pendingStorageSaveRef.current = null; }, [userId]); + // Keep a ref to saveMutation so the unmount cleanup can call it without + // capturing a stale closure. + const saveMutationRef = useRef(saveMutation); + saveMutationRef.current = saveMutation; + + // Flush any pending debounced DB save when the component unmounts so that + // navigating away within the 2-second window doesn't silently lose changes. useEffect(() => () => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = null; + if (pendingLayoutSaveRef.current) { + saveMutationRef.current.mutate({ layout: pendingLayoutSaveRef.current }); + pendingLayoutSaveRef.current = null; + } } }, []); @@ -159,8 +176,10 @@ export function useDashboardLayout() { } const newConfig = normalizeDashboardLayout(nextConfig); if (userId) saveToStorage(userId, newConfig); + pendingLayoutSaveRef.current = newConfig; if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); saveTimeoutRef.current = setTimeout(() => { + pendingLayoutSaveRef.current = null; saveMutation.mutate({ layout: newConfig }); }, 2000); }, [saveMutation, userId]);