From 721bc2c9f51dd04f4c5ae3ba90a0b1b5d04717e0 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 26 Apr 2026 23:05:51 +0000 Subject: [PATCH] fix: usePanelState returns string, preview decoupled from route Fixes React error #185 (infinite re-render loop) caused by returning object from Zustand selector without shallow comparison. Changed usePanelState to return string states that encode both preview and route status: - PREVIEW_CALCULATED: preview + calculated route - PREVIEW_ROUTING: preview + stops (no route yet) - PREVIEW: preview only - ROUTE_CALCULATED: calculated route only - ROUTING: stops only - IDLE: nothing Panel.jsx updated to derive show flags from string states using startsWith and includes checks. --- src/components/Panel.jsx | 12 ++++++------ src/store.js | 22 ++++++++++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/components/Panel.jsx b/src/components/Panel.jsx index 9cc28da..d6dd2a0 100644 --- a/src/components/Panel.jsx +++ b/src/components/Panel.jsx @@ -32,7 +32,7 @@ export default function Panel({ onManeuverClick }) { const activeTab = useStore((s) => s.activeTab) const setActiveTab = useStore((s) => s.setActiveTab) - const { hasPreview, routeState } = usePanelState() + const panelState = usePanelState() const [isMobile, setIsMobile] = useState(false) const [optimizing, setOptimizing] = useState(false) @@ -126,11 +126,11 @@ export default function Panel({ onManeuverClick }) { const showOptimize = effectiveCount >= 3 - // Determine what to show based on panel state (preview and route are now orthogonal) - const showPreviewCard = hasPreview - const showRouteSection = routeState === 'ROUTING' || routeState === 'CALCULATED' - const showManeuvers = routeState === 'CALCULATED' - const showEmptyState = !hasPreview && routeState === 'NONE' + // Determine what to show based on panel state + const showPreviewCard = panelState.startsWith('PREVIEW') + const showRouteSection = ['ROUTING', 'ROUTE_CALCULATED', 'PREVIEW_ROUTING', 'PREVIEW_CALCULATED'].includes(panelState) + const showManeuvers = panelState === 'ROUTE_CALCULATED' || panelState === 'PREVIEW_CALCULATED' + const showEmptyState = panelState === 'IDLE' // Routes tab content - now state-driven const routesContent = ( diff --git a/src/store.js b/src/store.js index dcc84cd..7949d95 100644 --- a/src/store.js +++ b/src/store.js @@ -1,5 +1,4 @@ import { create } from 'zustand' -import { shallow } from 'zustand/shallow' export const useStore = create((set, get) => ({ // ── Search state ── @@ -122,14 +121,21 @@ export const useStore = create((set, get) => ({ setActiveTab: (tab) => set({ activeTab: tab }), setEditingContact: (c) => set({ editingContact: c }), clearEditingContact: () => set({ editingContact: null }), -}), shallow) +})) // ── Panel state selector ── -// Returns { hasPreview: boolean, routeState: 'NONE' | 'ROUTING' | 'CALCULATED' } -// Preview and route states are now orthogonal - preview can show alongside any route state +// Returns string state, prioritizing preview to allow it alongside any route state export const usePanelState = () => { - return useStore((s) => ({ - hasPreview: !!s.selectedPlace, - routeState: s.route ? "CALCULATED" : (s.stops.length >= 1 ? "ROUTING" : "NONE") - }), shallow) + return useStore((s) => { + const hasPreview = !!s.selectedPlace + const hasRoute = !!s.route + const hasStops = s.stops.length >= 1 + + if (hasPreview && hasRoute) return "PREVIEW_CALCULATED" + if (hasPreview && hasStops) return "PREVIEW_ROUTING" + if (hasPreview) return "PREVIEW" + if (hasRoute) return "ROUTE_CALCULATED" + if (hasStops) return "ROUTING" + return "IDLE" + }) }