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 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 21:14:36 +02:00
parent c784b4b378
commit 5cc177ccf9
+19
View File
@@ -69,6 +69,8 @@ export function useDashboardLayout() {
const hasHydratedFromStorageRef = useRef(false); const hasHydratedFromStorageRef = useRef(false);
// Holds the DB config that needs to be persisted to localStorage once userId is available. // Holds the DB config that needs to be persisted to localStorage once userId is available.
const pendingStorageSaveRef = useRef<ReturnType<typeof normalizeDashboardLayout> | null>(null); const pendingStorageSaveRef = useRef<ReturnType<typeof normalizeDashboardLayout> | null>(null);
// Holds the latest layout that has been queued for DB save but not yet flushed.
const pendingLayoutSaveRef = useRef<DashboardLayoutConfig | null>(null);
// Once userId is known, hydrate from user-scoped localStorage (if no DB data yet). // Once userId is known, hydrate from user-scoped localStorage (if no DB data yet).
useEffect(() => { useEffect(() => {
@@ -79,6 +81,10 @@ export function useDashboardLayout() {
hasHydratedFromStorageRef.current = true; hasHydratedFromStorageRef.current = true;
if (stored) { if (stored) {
setConfig(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. // localStorage check is complete — show whatever we have now.
setIsHydrated(true); setIsHydrated(true);
@@ -146,10 +152,21 @@ export function useDashboardLayout() {
pendingStorageSaveRef.current = null; pendingStorageSaveRef.current = null;
}, [userId]); }, [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(() => () => { useEffect(() => () => {
if (saveTimeoutRef.current) { if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current); clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = null; 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); const newConfig = normalizeDashboardLayout(nextConfig);
if (userId) saveToStorage(userId, newConfig); if (userId) saveToStorage(userId, newConfig);
pendingLayoutSaveRef.current = newConfig;
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = setTimeout(() => { saveTimeoutRef.current = setTimeout(() => {
pendingLayoutSaveRef.current = null;
saveMutation.mutate({ layout: newConfig }); saveMutation.mutate({ layout: newConfig });
}, 2000); }, 2000);
}, [saveMutation, userId]); }, [saveMutation, userId]);