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:
@@ -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]);
|
||||||
|
|||||||
Reference in New Issue
Block a user