Browse Source

perf: replace bare useWorkflowStore() calls with selective subscriptions

WorkflowCanvas, Header, and FloatingActionBar all used
useWorkflowStore() without selectors, subscribing to the entire store.
Every hoveredNodeId change triggered re-renders in all three components
plus a full cascade through edges and nodes from WorkflowCanvas.

- WorkflowCanvas: split into individual selectors for clipboard state
- Header: wrap in useShallow with explicit field selection
- FloatingActionBar: wrap in useShallow with explicit field selection

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 4 months ago
parent
commit
d4130d62c9
  1. 17
      src/components/FloatingActionBar.tsx
  2. 18
      src/components/Header.tsx
  3. 5
      src/components/WorkflowCanvas.tsx

17
src/components/FloatingActionBar.tsx

@ -2,6 +2,7 @@
import { useRef, useState, useEffect, useMemo, useCallback } from "react";
import { useWorkflowStore } from "@/store/workflowStore";
import { useShallow } from "zustand/shallow";
import { NodeType } from "@/types";
import { useReactFlow } from "@xyflow/react";
import { ModelSearchDialog } from "./modals/ModelSearchDialog";
@ -320,7 +321,21 @@ export function FloatingActionBar() {
setModelSearchOpen,
modelSearchOpen,
modelSearchProvider,
} = useWorkflowStore();
} = useWorkflowStore(useShallow((state) => ({
nodes: state.nodes,
isRunning: state.isRunning,
currentNodeIds: state.currentNodeIds,
executeWorkflow: state.executeWorkflow,
regenerateNode: state.regenerateNode,
executeSelectedNodes: state.executeSelectedNodes,
stopWorkflow: state.stopWorkflow,
validateWorkflow: state.validateWorkflow,
edgeStyle: state.edgeStyle,
setEdgeStyle: state.setEdgeStyle,
setModelSearchOpen: state.setModelSearchOpen,
modelSearchOpen: state.modelSearchOpen,
modelSearchProvider: state.modelSearchProvider,
})));
// Get display text for running nodes
const runningNodeCount = currentNodeIds.length;

18
src/components/Header.tsx

@ -2,6 +2,7 @@
import { useState, useRef, useMemo, useCallback } from "react";
import { useWorkflowStore, WorkflowFile } from "@/store/workflowStore";
import { useShallow } from "zustand/shallow";
import { ProjectSetupModal } from "./ProjectSetupModal";
import { CostIndicator } from "./CostIndicator";
import { KeyboardShortcutsDialog } from "./KeyboardShortcutsDialog";
@ -73,7 +74,22 @@ export function Header() {
shortcutsDialogOpen,
setShortcutsDialogOpen,
setShowQuickstart,
} = useWorkflowStore();
} = useWorkflowStore(useShallow((state) => ({
workflowName: state.workflowName,
workflowId: state.workflowId,
saveDirectoryPath: state.saveDirectoryPath,
hasUnsavedChanges: state.hasUnsavedChanges,
lastSavedAt: state.lastSavedAt,
isSaving: state.isSaving,
setWorkflowMetadata: state.setWorkflowMetadata,
saveToFile: state.saveToFile,
loadWorkflow: state.loadWorkflow,
previousWorkflowSnapshot: state.previousWorkflowSnapshot,
revertToSnapshot: state.revertToSnapshot,
shortcutsDialogOpen: state.shortcutsDialogOpen,
setShortcutsDialogOpen: state.setShortcutsDialogOpen,
setShowQuickstart: state.setShowQuickstart,
})));
const [showProjectModal, setShowProjectModal] = useState(false);
const [projectModalMode, setProjectModalMode] = useState<"new" | "settings">("new");

5
src/components/WorkflowCanvas.tsx

@ -1283,7 +1283,10 @@ export function WorkflowCanvas() {
}, []);
// Get copy/paste functions and clipboard from store
const { copySelectedNodes, pasteNodes, clearClipboard, clipboard } = useWorkflowStore();
const copySelectedNodes = useWorkflowStore((state) => state.copySelectedNodes);
const pasteNodes = useWorkflowStore((state) => state.pasteNodes);
const clearClipboard = useWorkflowStore((state) => state.clearClipboard);
const clipboard = useWorkflowStore((state) => state.clipboard);
// Add non-passive wheel listener to handle zoom/pan and prevent browser navigation
// This replaces the onWheel prop which is passive by default and can't preventDefault

Loading…
Cancel
Save