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.
This commit is contained in:
Matt 2026-04-26 23:05:51 +00:00
commit 721bc2c9f5
2 changed files with 20 additions and 14 deletions

View file

@ -32,7 +32,7 @@ export default function Panel({ onManeuverClick }) {
const activeTab = useStore((s) => s.activeTab) const activeTab = useStore((s) => s.activeTab)
const setActiveTab = useStore((s) => s.setActiveTab) const setActiveTab = useStore((s) => s.setActiveTab)
const { hasPreview, routeState } = usePanelState() const panelState = usePanelState()
const [isMobile, setIsMobile] = useState(false) const [isMobile, setIsMobile] = useState(false)
const [optimizing, setOptimizing] = useState(false) const [optimizing, setOptimizing] = useState(false)
@ -126,11 +126,11 @@ export default function Panel({ onManeuverClick }) {
const showOptimize = effectiveCount >= 3 const showOptimize = effectiveCount >= 3
// Determine what to show based on panel state (preview and route are now orthogonal) // Determine what to show based on panel state
const showPreviewCard = hasPreview const showPreviewCard = panelState.startsWith('PREVIEW')
const showRouteSection = routeState === 'ROUTING' || routeState === 'CALCULATED' const showRouteSection = ['ROUTING', 'ROUTE_CALCULATED', 'PREVIEW_ROUTING', 'PREVIEW_CALCULATED'].includes(panelState)
const showManeuvers = routeState === 'CALCULATED' const showManeuvers = panelState === 'ROUTE_CALCULATED' || panelState === 'PREVIEW_CALCULATED'
const showEmptyState = !hasPreview && routeState === 'NONE' const showEmptyState = panelState === 'IDLE'
// Routes tab content - now state-driven // Routes tab content - now state-driven
const routesContent = ( const routesContent = (

View file

@ -1,5 +1,4 @@
import { create } from 'zustand' import { create } from 'zustand'
import { shallow } from 'zustand/shallow'
export const useStore = create((set, get) => ({ export const useStore = create((set, get) => ({
// ── Search state ── // ── Search state ──
@ -122,14 +121,21 @@ export const useStore = create((set, get) => ({
setActiveTab: (tab) => set({ activeTab: tab }), setActiveTab: (tab) => set({ activeTab: tab }),
setEditingContact: (c) => set({ editingContact: c }), setEditingContact: (c) => set({ editingContact: c }),
clearEditingContact: () => set({ editingContact: null }), clearEditingContact: () => set({ editingContact: null }),
}), shallow) }))
// ── Panel state selector ── // ── Panel state selector ──
// Returns { hasPreview: boolean, routeState: 'NONE' | 'ROUTING' | 'CALCULATED' } // Returns string state, prioritizing preview to allow it alongside any route state
// Preview and route states are now orthogonal - preview can show alongside any route state
export const usePanelState = () => { export const usePanelState = () => {
return useStore((s) => ({ return useStore((s) => {
hasPreview: !!s.selectedPlace, const hasPreview = !!s.selectedPlace
routeState: s.route ? "CALCULATED" : (s.stops.length >= 1 ? "ROUTING" : "NONE") const hasRoute = !!s.route
}), shallow) 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"
})
} }