From a36617f9e34f89f2211a0226b8ab40ea9c8fed64 Mon Sep 17 00:00:00 2001 From: josh Date: Fri, 24 Apr 2026 20:29:54 -0400 Subject: [PATCH] Fix max update depth crash on Infrastructure page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ToastContainer's useEffect had `seen` (a Set state) in its dependency array — each setSeen created a new Set, re-fired the effect, and under rapid notifications cascaded past React's 50-update limit. Replaced with a ref since seen doesn't need to trigger re-renders. Also added useShallow to DataCenterCard's pipeline selector to prevent .filter() from causing spurious re-renders on unrelated store changes. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/components/common/ToastContainer.tsx | 14 +++++--------- apps/web/src/pages/InfrastructurePage.tsx | 5 +++-- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/apps/web/src/components/common/ToastContainer.tsx b/apps/web/src/components/common/ToastContainer.tsx index fff5d50..4643e28 100644 --- a/apps/web/src/components/common/ToastContainer.tsx +++ b/apps/web/src/components/common/ToastContainer.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; import { X, CheckCircle, AlertTriangle, Info, AlertCircle } from 'lucide-react'; import { useGameStore, type GameNotification } from '@/store'; @@ -10,23 +10,19 @@ export function ToastContainer() { const notifications = useGameStore((s) => s.notifications); const dismissNotification = useGameStore((s) => s.dismissNotification); const [toasts, setToasts] = useState([]); - const [seen, setSeen] = useState(new Set()); + const seenRef = useRef(new Set()); useEffect(() => { - const newNotifs = notifications.filter(n => !n.read && !seen.has(n.id)); + const newNotifs = notifications.filter(n => !n.read && !seenRef.current.has(n.id)); if (newNotifs.length === 0) return; - setSeen(prev => { - const next = new Set(prev); - for (const n of newNotifs) next.add(n.id); - return next; - }); + for (const n of newNotifs) seenRef.current.add(n.id); setToasts(prev => [ ...newNotifs.map(n => ({ ...n, exiting: false })), ...prev, ].slice(0, 5)); - }, [notifications, seen]); + }, [notifications]); useEffect(() => { if (toasts.length === 0) return; diff --git a/apps/web/src/pages/InfrastructurePage.tsx b/apps/web/src/pages/InfrastructurePage.tsx index 9e12756..23e3727 100644 --- a/apps/web/src/pages/InfrastructurePage.tsx +++ b/apps/web/src/pages/InfrastructurePage.tsx @@ -1,6 +1,7 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { Plus, Server, MapPin, Zap, HardDrive, Wrench, ChevronDown, ChevronUp, Thermometer, Shield, X } from 'lucide-react'; import { useGameStore } from '@/store'; +import { useShallow } from 'zustand/shallow'; import { formatMoney, formatNumber, formatPercent, LOCATION_CONFIGS, DC_TIER_CONFIGS, RACK_SKU_CONFIGS, @@ -105,7 +106,7 @@ function CapacityBar({ label, used, max, unit, icon: Icon }: { function DataCenterCard({ dcId }: { dcId: string }) { const dc = useGameStore((s) => s.infrastructure.dataCenters.find(d => d.id === dcId))!; - const pipelineForDc = useGameStore((s) => s.infrastructure.rackPipeline.filter(o => o.dataCenterId === dcId)); + const pipelineForDc = useGameStore(useShallow((s) => s.infrastructure.rackPipeline.filter(o => o.dataCenterId === dcId))); const money = useGameStore((s) => s.economy.money); const era = useGameStore((s) => s.meta.currentEra); const completedResearch = useGameStore((s) => s.research.completedResearch);