From ed336331240e09d14f5597e4d9b3e3ff91f537a9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 30 Jun 2026 04:34:18 +0000 Subject: [PATCH] Refactor Zustand usage and add React.memo Refactored CanvasArea, Sidebar, and Toolbar components to use granular Zustand selectors instead of destructuring the state object. Added React.memo to ElementWrapper to prevent O(N) canvas re-renders. Co-authored-by: sourabhrajcodes <95199723+sourabhrajcodes@users.noreply.github.com> --- .jules/bolt.md | 3 +++ frontend/src/components/CanvasArea.tsx | 16 +++++++++++++--- frontend/src/components/Sidebar.tsx | 9 ++++++++- frontend/src/components/Toolbar.tsx | 6 +++++- 4 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 .jules/bolt.md diff --git a/.jules/bolt.md b/.jules/bolt.md new file mode 100644 index 0000000..dbac76a --- /dev/null +++ b/.jules/bolt.md @@ -0,0 +1,3 @@ +## 2024-06-30 - Granular Zustand Selectors and Konva Performance +**Learning:** Destructuring the entire Zustand state object (e.g., `const { a, b } = useStore()`) inside React components causes the component to re-render whenever *any* state in the store changes, rather than just the properties being used. Furthermore, in React Konva applications, updating a single property on one canvas element can trigger O(N) re-renders for all elements on the canvas unless wrapped properly. +**Action:** Always use granular selectors (e.g., `const a = useStore(state => state.a)`) instead of destructuring the state object. Always wrap list items, especially Konva canvas elements (`ElementWrapper`), in `React.memo` to prevent O(N) re-renders when a single element updates. diff --git a/frontend/src/components/CanvasArea.tsx b/frontend/src/components/CanvasArea.tsx index ae81602..676f41b 100644 --- a/frontend/src/components/CanvasArea.tsx +++ b/frontend/src/components/CanvasArea.tsx @@ -10,7 +10,10 @@ interface ElementWrapperProps { onChange: (newAttrs: Partial) => void; } -const ElementWrapper: React.FC = ({ element, isSelected, onSelect, onChange }) => { +// ⚡ BOLT OPTIMIZATION: Wrap ElementWrapper in React.memo +// Prevents O(N) re-renders of all canvas elements when a single element is moved or selected. +// Expected measurable impact: Drastically reduces CPU time and frame drops during drag/drop or selection in documents with many elements. +const ElementWrapper: React.FC = React.memo(({ element, isSelected, onSelect, onChange }) => { const shapeRef = useRef(null); const trRef = useRef(null); @@ -71,10 +74,17 @@ const ElementWrapper: React.FC = ({ element, isSelected, on // Placeholder for tables return null; -}; +}); export const CanvasArea = () => { - const { pages, activePageId, selectedElementId, selectElement, updateElement } = useEditorStore(); + // ⚡ BOLT OPTIMIZATION: Use granular selectors for Zustand state instead of destructuring the entire state object. + // This prevents unnecessary re-renders when other un-related state properties change. + // Expected measurable impact: Eliminates unnecessary re-renders of the CanvasArea component when unrelated state changes. + const pages = useEditorStore(state => state.pages); + const activePageId = useEditorStore(state => state.activePageId); + const selectedElementId = useEditorStore(state => state.selectedElementId); + const selectElement = useEditorStore(state => state.selectElement); + const updateElement = useEditorStore(state => state.updateElement); const activePage = pages.find(p => p.id === activePageId) || pages[0]; const [scale] = useState(1); diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index 8ec7193..99a6347 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -1,7 +1,14 @@ import { useEditorStore } from '../store/useEditorStore'; export const Sidebar = () => { - const { pages, activePageId, addPage, setActivePage, selectedElementId } = useEditorStore(); + // ⚡ BOLT OPTIMIZATION: Use granular selectors for Zustand state instead of destructuring the entire state object. + // This prevents unnecessary re-renders when other un-related state properties change. + // Expected measurable impact: Eliminates unnecessary re-renders of the Sidebar component when unrelated state changes. + const pages = useEditorStore(state => state.pages); + const activePageId = useEditorStore(state => state.activePageId); + const addPage = useEditorStore(state => state.addPage); + const setActivePage = useEditorStore(state => state.setActivePage); + const selectedElementId = useEditorStore(state => state.selectedElementId); return (
diff --git a/frontend/src/components/Toolbar.tsx b/frontend/src/components/Toolbar.tsx index efd1490..7ff1ac8 100644 --- a/frontend/src/components/Toolbar.tsx +++ b/frontend/src/components/Toolbar.tsx @@ -3,7 +3,11 @@ import { useEditorStore } from '../store/useEditorStore'; import type { TextElement } from '../types'; export const Toolbar = () => { - const { addElement, activePageId } = useEditorStore(); + // ⚡ BOLT OPTIMIZATION: Use granular selectors for Zustand state instead of destructuring the entire state object. + // This prevents unnecessary re-renders when other un-related state properties change. + // Expected measurable impact: Eliminates unnecessary re-renders of the Toolbar component when unrelated state changes. + const addElement = useEditorStore(state => state.addElement); + const activePageId = useEditorStore(state => state.activePageId); const handleAddText = () => { if (!activePageId) return;