Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .jules/bolt.md
Original file line number Diff line number Diff line change
@@ -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.
16 changes: 13 additions & 3 deletions frontend/src/components/CanvasArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ interface ElementWrapperProps {
onChange: (newAttrs: Partial<PageElement>) => void;
}

const ElementWrapper: React.FC<ElementWrapperProps> = ({ 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<ElementWrapperProps> = React.memo(({ element, isSelected, onSelect, onChange }) => {
const shapeRef = useRef<any>(null);
const trRef = useRef<any>(null);

Expand Down Expand Up @@ -71,10 +74,17 @@ const ElementWrapper: React.FC<ElementWrapperProps> = ({ 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);
Expand Down
9 changes: 8 additions & 1 deletion frontend/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="w-64 bg-white border-r flex flex-col h-full shadow-sm">
Expand Down
6 changes: 5 additions & 1 deletion frontend/src/components/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down