From 45884cbab70c3b3cc4e0950296806549cda4041f Mon Sep 17 00:00:00 2001 From: iotserver24 <147928812+iotserver24@users.noreply.github.com> Date: Mon, 4 May 2026 17:43:30 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Prevent=20O(N)=20cascading?= =?UTF-8?q?=20re-renders=20in=20FileExplorer=20during=20timer=20ticks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/bolt.md | 4 ++++ .../src/renderer/components/FileExplorer.tsx | 23 +++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 0e8670e..678e65a 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -31,3 +31,7 @@ ## 2026-05-03 - [O(N) Cascade re-renders in Lists with State-Driven Hover] **Learning:** Prop-drilling or managing local interactive state (like `hoveredId` on `mouseenter`/`mouseleave`) within a list component mapping an array of items (like `ChatHistory` mapping `SessionItem`s) creates O(N) re-renders when interacting with any single item. **Action:** Always favor native CSS pseudo-classes like `:hover` or Tailwind's `group-hover` over interactive React state for visually toggling elements inside rendered lists, and wrap the parent list component with `React.memo` if it sits alongside frequently updating sibling states (like a timer or streaming tokens). + +## 2024-05-04 - Unnecessary Timer Re-renders +**Learning:** App.tsx has a 250ms setInterval updating state (runElapsed) while the agent is running, which forces the entire app tree (including large lists in FileExplorer and ChatPanel) to re-render 4 times a second. +**Action:** Always wrap large list components and recursive tree components (like FileExplorer's Row) in React.memo() when they are descendants of a component with high-frequency state updates. Also use useMemo for mapping large lists (like chat messages) to prevent unnecessary VDOM recreation. diff --git a/packages/desktop/src/renderer/components/FileExplorer.tsx b/packages/desktop/src/renderer/components/FileExplorer.tsx index 4eb9b9d..b788b56 100644 --- a/packages/desktop/src/renderer/components/FileExplorer.tsx +++ b/packages/desktop/src/renderer/components/FileExplorer.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, memo } from 'react'; interface FileExplorerProps { workingDir: string; @@ -32,7 +32,8 @@ function color(name: string, dir: boolean): string { return 'text-xibe-text-dim'; } -function Row({ node, depth, open, kids, onToggle }: { node: FileEntry; depth: number; open: boolean; kids?: FileEntry[]; onToggle: (n: FileEntry) => void }) { +/* ⚡ Bolt: Memoized Row component to prevent O(N) re-renders of the file tree on timer ticks */ +const Row = memo(function Row({ node, depth, open, kids, onToggle }: { node: FileEntry; depth: number; open: boolean; kids?: FileEntry[]; onToggle: (n: FileEntry) => void }) { return (
); -} +}); -export default function FileExplorer({ workingDir }: FileExplorerProps) { +export default memo(function FileExplorer({ workingDir }: FileExplorerProps) { const [tree, setTree] = useState([]); const [expanded, setExpanded] = useState>(new Set()); @@ -63,16 +64,18 @@ export default function FileExplorer({ workingDir }: FileExplorerProps) { useEffect(() => { load(workingDir).then(setTree); }, [workingDir, load]); - const toggle = async (node: FileEntry) => { + const toggle = useCallback(async (node: FileEntry) => { if (!node.isDirectory) return; - const next = new Set(expanded); - next.has(node.path) ? next.delete(node.path) : next.add(node.path); - setExpanded(next); + setExpanded((prev) => { + const next = new Set(prev); + next.has(node.path) ? next.delete(node.path) : next.add(node.path); + return next; + }); if (!node.loaded) { const children = await load(node.path); setTree((prev) => update(prev, node.path, { children, loaded: true })); } - }; + }, [load]); return (
@@ -82,4 +85,4 @@ export default function FileExplorer({ workingDir }: FileExplorerProps) {
); -} +});