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 <noreply@anthropic.com>
This commit is contained in:
Nicolaj Hartmann 2026-01-30 23:40:05 +01:00
parent de235a3da7
commit effb06fc44
3 changed files with 79 additions and 13 deletions

View file

@ -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 (
<Spin
size="large"
tip="Indlæser..."
style={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
}}
>
<div style={{ minHeight: 200 }} />
</Spin>
);
}
function App() {
return (
<AntApp>
<BrowserRouter>
<Routes>
{/* Wizard route OUTSIDE CompanyGuard to prevent navigation loop */}
<Route
path="/opret-virksomhed"
element={
<ProtectedRoute>
<Suspense fallback={<PageLoader />}>
<CompanySetupWizard />
</Suspense>
</ProtectedRoute>
}
/>
{/* All other routes with full guard chain */}
<Route
path="/*"
element={
<ProtectedRoute>
<CompanyGuard>
<AppLayout>
<AppRoutes />
</AppLayout>
</CompanyGuard>
</ProtectedRoute>
}
/>
</Routes>
</BrowserRouter>
</AntApp>
);

View file

@ -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) {

View file

@ -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 */}
<Route path="/admin" element={<Admin />} />
{/* Company setup wizard is now in App.tsx (outside CompanyGuard) */}
{/* Fallback redirect */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>