From effb06fc440e145769f392e0c00929207ef04ea1 Mon Sep 17 00:00:00 2001 From: Nicolaj Hartmann Date: Fri, 30 Jan 2026 23:40:05 +0100 Subject: [PATCH] Fix infinite navigation loop in CompanyGuard - Add hasNavigatedRef to prevent multiple navigation calls - Move /opret-virksomhed route outside CompanyGuard in App.tsx - Reduce retry count and disable refetchOnWindowFocus to limit re-renders - Remove activeCompany from useEffect dependencies (use getState() instead) This fixes "Too many calls to Location or History APIs" error that caused the app to crash with DOMException. Co-Authored-By: Claude Opus 4.5 --- frontend/src/App.tsx | 58 +++++++++++++++++-- frontend/src/components/auth/CompanyGuard.tsx | 31 +++++++--- frontend/src/routes.tsx | 3 + 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fc3f2d7..12de8d1 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,18 +1,64 @@ -import { BrowserRouter } from 'react-router-dom'; +import { BrowserRouter, Routes, Route } from 'react-router-dom'; import { App as AntApp } from 'antd'; +import { Suspense, lazy } from 'react'; +import { Spin } from 'antd'; import AppRoutes from './routes'; import AppLayout from './components/layout/AppLayout'; import ProtectedRoute from './components/auth/ProtectedRoute'; +import CompanyGuard from './components/auth/CompanyGuard'; + +// Lazy load CompanySetupWizard +const CompanySetupWizard = lazy(() => import('./pages/CompanySetupWizard')); + +// Loading fallback component +function PageLoader() { + return ( + +
+ + ); +} function App() { return ( - - - - - + + {/* Wizard route OUTSIDE CompanyGuard to prevent navigation loop */} + + }> + + + + } + /> + + {/* All other routes with full guard chain */} + + + + + + + + } + /> + ); diff --git a/frontend/src/components/auth/CompanyGuard.tsx b/frontend/src/components/auth/CompanyGuard.tsx index c5f1e29..78372e9 100644 --- a/frontend/src/components/auth/CompanyGuard.tsx +++ b/frontend/src/components/auth/CompanyGuard.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect } from 'react'; +import { ReactNode, useEffect, useRef } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { Spin, Typography } from 'antd'; import { useMyCompanies } from '@/api/queries/companyQueries'; @@ -17,7 +17,8 @@ interface CompanyGuardProps { export default function CompanyGuard({ children }: CompanyGuardProps) { const navigate = useNavigate(); const location = useLocation(); - const { setCompanies, setActiveCompany, activeCompany } = useCompanyStore(); + const { setCompanies, setActiveCompany } = useCompanyStore(); + const hasNavigatedRef = useRef(false); const { data: companies, @@ -27,6 +28,8 @@ export default function CompanyGuard({ children }: CompanyGuardProps) { } = useMyCompanies({ staleTime: 5 * 60 * 1000, refetchOnMount: true, + retry: 1, + refetchOnWindowFocus: false, }); // Sync companies to store when loaded @@ -34,32 +37,46 @@ export default function CompanyGuard({ children }: CompanyGuardProps) { if (companies && companies.length > 0) { setCompanies(companies); + // Get current value without adding to dependencies to avoid infinite loop + const current = useCompanyStore.getState().activeCompany; + // Validate that activeCompany exists in API response - const validCompany = activeCompany - ? companies.find((c) => c.id === activeCompany.id) + const validCompany = current + ? companies.find((c) => c.id === current.id) : null; if (!validCompany) { // Persisted company doesn't exist - set to first setActiveCompany(companies[0]); - } else if (JSON.stringify(validCompany) !== JSON.stringify(activeCompany)) { + } else if (JSON.stringify(validCompany) !== JSON.stringify(current)) { // Update with fresh data from API setActiveCompany(validCompany); } } - }, [companies, activeCompany, setCompanies, setActiveCompany]); + }, [companies, setCompanies, setActiveCompany]); // Redirect to wizard if no companies and not already on wizard page useEffect(() => { + // Guard against multiple navigations to prevent infinite loop + if (hasNavigatedRef.current) return; + if (!isLoading && companies !== undefined) { const isOnWizard = location.pathname === '/opret-virksomhed'; if (companies.length === 0 && !isOnWizard) { + hasNavigatedRef.current = true; navigate('/opret-virksomhed', { replace: true }); } // Note: Users with existing companies CAN access the wizard to create more } - }, [companies, isLoading, location.pathname, navigate]); + }, [companies, isLoading, navigate, location.pathname]); + + // Reset navigation ref when companies change (user created a company) + useEffect(() => { + if (companies && companies.length > 0) { + hasNavigatedRef.current = false; + } + }, [companies]); // Show loading state if (isLoading) { diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 0d1117d..5341654 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -13,6 +13,7 @@ const Eksport = lazy(() => import('./pages/Eksport')); const Settings = lazy(() => import('./pages/Settings')); const UserSettings = lazy(() => import('./pages/UserSettings')); const Admin = lazy(() => import('./pages/Admin')); +// CompanySetupWizard moved to App.tsx (outside CompanyGuard) // Invoicing pages const Kunder = lazy(() => import('./pages/Kunder')); @@ -73,6 +74,8 @@ export default function AppRoutes() { {/* Admin */} } /> + {/* Company setup wizard is now in App.tsx (outside CompanyGuard) */} + {/* Fallback redirect */} } />