From 8feccb6a488058087a695647f0820a2d61326abc Mon Sep 17 00:00:00 2001 From: Jan-Henrik Bruhn Date: Thu, 5 Feb 2026 15:03:49 +0100 Subject: [PATCH] Fix store subscription storm with shallow equality MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical Performance Issue: - Every CustomNode and CustomEdge subscribed to entire store arrays - 100 nodes × 2 subscriptions + 200 edges × 3 subscriptions = 800 listeners - ANY change to nodeTypes/labels/edgeTypes triggered ALL components to re-render - Example: Changing one node type color → 300 components re-render Solution: - Add shallow equality checking to all store subscriptions - Components now only re-render when array CONTENTS change - Prevents cascade re-renders from reference changes Files Modified: - CustomNode.tsx: nodeTypes, labels with shallow - CustomEdge.tsx: edgeTypes, labels, nodeTypes with shallow Expected Impact: - Eliminates unnecessary re-renders during viewport changes - Should dramatically improve responsiveness during pan/zoom - Reduces re-render churn when editing types/labels Co-Authored-By: Claude Sonnet 4.5 --- src/components/Edges/CustomEdge.tsx | 7 ++++--- src/components/Nodes/CustomNode.tsx | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/Edges/CustomEdge.tsx b/src/components/Edges/CustomEdge.tsx index 3bdc47e..2946dcc 100644 --- a/src/components/Edges/CustomEdge.tsx +++ b/src/components/Edges/CustomEdge.tsx @@ -6,6 +6,7 @@ import { useInternalNode, } from '@xyflow/react'; import { useGraphStore } from '../../stores/graphStore'; +import { shallow } from 'zustand/shallow'; import type { Relation } from '../../types'; import LabelBadge from '../Common/LabelBadge'; import { getFloatingEdgeParams } from '../../utils/edgeUtils'; @@ -35,9 +36,9 @@ const CustomEdge = ({ data, selected, }: EdgeProps) => { - const edgeTypes = useGraphStore((state) => state.edgeTypes); - const labels = useGraphStore((state) => state.labels); - const nodeTypes = useGraphStore((state) => state.nodeTypes); + const edgeTypes = useGraphStore((state) => state.edgeTypes, shallow); + const labels = useGraphStore((state) => state.labels, shallow); + const nodeTypes = useGraphStore((state) => state.nodeTypes, shallow); // Get active filters based on mode (editing vs presentation) const filters = useActiveFilters(); diff --git a/src/components/Nodes/CustomNode.tsx b/src/components/Nodes/CustomNode.tsx index 4075a55..4ee56d2 100644 --- a/src/components/Nodes/CustomNode.tsx +++ b/src/components/Nodes/CustomNode.tsx @@ -1,6 +1,7 @@ import { memo, useMemo } from "react"; import { Handle, Position, NodeProps } from "@xyflow/react"; import { useGraphStore } from "../../stores/graphStore"; +import { shallow } from "zustand/shallow"; import { getContrastColor, adjustColorBrightness, @@ -26,8 +27,8 @@ import { * Usage: Automatically rendered by React Flow for nodes with type='custom' */ const CustomNode = ({ data, selected }: NodeProps) => { - const nodeTypes = useGraphStore((state) => state.nodeTypes); - const labels = useGraphStore((state) => state.labels); + const nodeTypes = useGraphStore((state) => state.nodeTypes, shallow); + const labels = useGraphStore((state) => state.labels, shallow); // Get active filters based on mode (editing vs presentation) const filters = useActiveFilters();