From acab4fd5bd1a24048f33580acf36a81e2238c995 Mon Sep 17 00:00:00 2001 From: fathiaoyinloye Date: Mon, 29 Jun 2026 17:35:47 +0100 Subject: [PATCH 1/3] feat debounce localStorage writes for dashboard layout --- TODO.md | 8 ++++++ src/app/hooks/useDashboardWidgets.tsx | 36 ++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..c481e46e --- /dev/null +++ b/TODO.md @@ -0,0 +1,8 @@ +# Task Tracker + +- [x] Implement scoped 500ms trailing debounce in `src/app/hooks/useDashboardWidgets.tsx` to rate-limit localStorage writes. +- [ ] (Optional) Prevent initial-load persistence writes in `src/app/components/dashboard/DashboardGrid.tsx` (skip first save after mount). +- [ ] Run tests/lint if applicable. +- [ ] Manual verification: Chrome DevTools Performance panel during drag; confirm no long tasks from localStorage I/O. + + diff --git a/src/app/hooks/useDashboardWidgets.tsx b/src/app/hooks/useDashboardWidgets.tsx index c0a37905..619e2981 100644 --- a/src/app/hooks/useDashboardWidgets.tsx +++ b/src/app/hooks/useDashboardWidgets.tsx @@ -1,4 +1,5 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; + interface Widget { id: string; @@ -27,15 +28,42 @@ export const useDashboardWidgets = () => { return []; }, []); - // Save widget layout to localStorage + const saveTimerRef = useRef | null>(null); + const pendingWidgetsRef = useRef(null); + + useEffect(() => { + return () => { + if (saveTimerRef.current) clearTimeout(saveTimerRef.current); + }; + }, []); + const saveWidgetLayout = useCallback((widgets: Widget[]) => { + // Trailing debounce to avoid blocking the main thread during drag-and-drop. + // localStorage writes will happen at most once per 500ms, and the latest + // layout will be persisted within ~500ms after the last change. try { - localStorage.setItem('dashboard-widgets', JSON.stringify(widgets)); + pendingWidgetsRef.current = widgets; + + if (saveTimerRef.current) clearTimeout(saveTimerRef.current); + + saveTimerRef.current = setTimeout(() => { + try { + const pending = pendingWidgetsRef.current; + if (!pending) return; + localStorage.setItem('dashboard-widgets', JSON.stringify(pending)); + } catch (error) { + console.error('Failed to save widget layout:', error); + } finally { + saveTimerRef.current = null; + } + }, 500); } catch (error) { - console.error('Failed to save widget layout:', error); + console.error('Failed to schedule dashboard layout save:', error); } }, []); + + // Initialize widgets on mount useEffect(() => { const savedWidgets = loadWidgetLayout(); From fba10a912f80bbf589d199f51c752c521e45ff17 Mon Sep 17 00:00:00 2001 From: fathiaoyinloye Date: Tue, 30 Jun 2026 06:52:28 +0100 Subject: [PATCH 2/3] perf: debounce DashboardGrid localStorage writes --- .../components/dashboard/DashboardGrid.tsx | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/src/app/components/dashboard/DashboardGrid.tsx b/src/app/components/dashboard/DashboardGrid.tsx index 1c97fba6..fe9b6924 100644 --- a/src/app/components/dashboard/DashboardGrid.tsx +++ b/src/app/components/dashboard/DashboardGrid.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { DndContext, @@ -104,6 +104,14 @@ export const DashboardGrid: React.FC = ({ const [activeTab, setActiveTab] = useState('overview'); const [dateRange] = useState('Last 30 days'); + const saveTimerRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (saveTimerRef.current) clearTimeout(saveTimerRef.current); + }; + }, []); + const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -111,7 +119,7 @@ export const DashboardGrid: React.FC = ({ }), ); - const { saveWidgetLayout, loadWidgetLayout } = useDashboardWidgets(); + const { loadWidgetLayout } = useDashboardWidgets(); // Load saved layout on mount useEffect(() => { @@ -129,18 +137,32 @@ export const DashboardGrid: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Only run on mount - // Save layout when widgets change (but not on initial load) + // Notify parent of widget changes immediately (no I/O) useEffect(() => { - if (widgets.length === 0) return; // Don't save empty state + if (widgets.length === 0) return; + onWidgetChange?.(widgets); + }, [widgets, onWidgetChange]); - try { - saveWidgetLayout(widgets); - onWidgetChange?.(widgets); - } catch (error) { - console.error('Error saving widget layout', error); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [widgets]); // Only depend on widgets, not the functions + // Persist layout to localStorage at most once per 500ms via debounce + useEffect(() => { + if (widgets.length === 0) return; + + if (saveTimerRef.current) clearTimeout(saveTimerRef.current); + + saveTimerRef.current = setTimeout(() => { + try { + localStorage.setItem('dashboard-widgets', JSON.stringify(widgets)); + } catch (error) { + console.error('Error saving widget layout', error); + } finally { + saveTimerRef.current = null; + } + }, 500); + + return () => { + if (saveTimerRef.current) clearTimeout(saveTimerRef.current); + }; + }, [widgets]); const handleDragEnd = (event: DragEndEvent) => { const { active, over } = event; From 545f1ce82a04709cb609d308044197144f34a55d Mon Sep 17 00:00:00 2001 From: fathiaoyinloye Date: Tue, 30 Jun 2026 07:11:04 +0100 Subject: [PATCH 3/3] fix: prettier formatting in useDashboardWidgets --- src/app/hooks/useDashboardWidgets.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app/hooks/useDashboardWidgets.tsx b/src/app/hooks/useDashboardWidgets.tsx index 619e2981..c65563b4 100644 --- a/src/app/hooks/useDashboardWidgets.tsx +++ b/src/app/hooks/useDashboardWidgets.tsx @@ -1,6 +1,5 @@ import { useState, useEffect, useCallback, useRef } from 'react'; - interface Widget { id: string; type: string; @@ -62,8 +61,6 @@ export const useDashboardWidgets = () => { } }, []); - - // Initialize widgets on mount useEffect(() => { const savedWidgets = loadWidgetLayout();