Fix React Router Infinite Redirect Loop on /stuauth & Root
Solve React Router infinite redirect loops on /stuauth and unexpected redirects from root path in createBrowserRouter with authentication layouts. Guarded redirects, hydration checks, and route loaders prevent loops in StudentLayout.
React Router infinite redirect loop on /stuauth and unexpected redirect from root path (/) in BrowserRouter with authentication layouts
I’m experiencing routing issues in my React app using createBrowserRouter from react-router-dom.
Problem description:
- Accessing
localhost:5173/immediately triggers a student authentication check and redirects to/stuauth. - On
/stuauth, it enters an infinite redirect loop to the same page.
The issue seems related to the StudentLayout, but I can’t pinpoint the cause. Here’s my complete router.tsx file:
import React from "react";
import {
createBrowserRouter,
RouterProvider,
Outlet,
Navigate,
useLocation,
} from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AuthProvider, useAuth } from "../shared/contexts/AuthContext";
import Protected from "../shared/components/Protected";
import Sidebar from "../shared/components/Sidebar";
import AppErrorBoundary from "../shared/components/AppErrorBoundary";
import NotFound from "../shared/components/NotFound";
import StudentAuthPage from "../modules/auth/pages/StudentAuthPage";
// ... (all other imports)
const StudentLayout: React.FC = () => {
const { student, hydrated } = useAuth();
const loc = useLocation();
if (!hydrated) return null;
if (!student) {
return <Navigate to="/stuauth" replace />;
}
return (
<Protected role="student">
<div className="min-h-screen flex">
<Sidebar variant="student" />
<main className="flex-1 p-4 md:p-6">
<Outlet />
</main>
</div>
</Protected>
);
};
// AdminLayout, PublicLayout definitions...
const router = createBrowserRouter([
{
element: <PublicLayout />,
children: [
{ path: "/", element: <LandingPage /> },
{ path: "/stuauth", element: <StudentAuthPage /> },
{ path: "/adlogin", element: <AdminLoginPage /> },
],
},
{
path: "/student",
element: <StudentLayout />,
children: [
// student routes...
],
},
{
path: "/admin",
element: <AdminLayout />,
children: [
// admin routes...
],
},
{
element: <PublicLayout />,
children: [{ path: "*", element: <NotFound /> }],
},
]);
const qc = new QueryClient();
const AppRouter: React.FC = () => (
<QueryClientProvider client={qc}>
<AuthProvider>
<AppErrorBoundary>
<RouterProvider router={router} />
</AppErrorBoundary>
</AuthProvider>
</QueryClientProvider>
);
export default AppRouter;
Key observations:
- Root
/is underPublicLayoutwithLandingPage, which shouldn’t check student auth. StudentLayoutredirects to/stuauthif nostudentin auth context./stuauthis also underPublicLayout.
How can I fix the infinite loop on /stuauth and prevent the unexpected redirect from /?
StudentLayout is performing a layout-level auth redirect while the auth state (or the current location) isn’t checked, so visiting / or /stuauth can trigger a Navigate loop. Fix it by ensuring /stuauth is only served from your public routes (not mounted inside StudentLayout), guarding the StudentLayout redirect with a location check (for example if (!student && !loc.pathname.startsWith('/stuauth')) return <Navigate to="/stuauth" state={{ from: loc }} replace />;), and making sure your AuthProvider exposes a proper hydrated flag (don’t redirect until hydration finishes); as an alternative, perform auth checks in route loaders to avoid mounting the layout at all.
Contents
- Diagnosis: why React Router redirect loop happens
- Fix the infinite redirect loop on /stuauth (StudentLayout)
- Prevent unexpected redirect from root
/ - Alternative: route loaders and Protected patterns
- Debugging checklist
- Sources
- Conclusion
React Router redirect loop with createBrowserRouter — quick diagnosis
What’s happening, in plain terms: StudentLayout contains an unconditional redirect for unauthenticated users:
if (!student) return
If StudentLayout ever mounts while the current location is /stuauth (i.e., the auth page is being rendered inside that layout), the layout will redirect to /stuauth again — a classic infinite Navigate loop. The same pattern will produce an “unexpected redirect from /” when StudentLayout gets evaluated for the root route prematurely (for example because auth hasn’t finished hydrating, a global guard runs too early, or your route configuration mounts the wrong layout).
Common root causes:
- /stuauth is accidentally being rendered inside (or beneath) StudentLayout instead of only in your public routes. (Keep auth pages public.)
- The layout redirect runs before your AuthProvider has finished hydrating auth state (hydrated is false/true at the wrong time).
- A component (Protected or another global guard) calls navigate inside useEffect without checking the current location, which can fire on pages that shouldn’t be protected.
- Duplicate pathless layouts or misordered routes produce unexpected mounts.
For patterns and examples of guarded routes and the location-check solution see the writeups on protected routes and authentication (for example https://ui.dev/react-router-protected-routes-authentication and https://www.robinwieruch.de/react-router-authentication).
Fix the infinite redirect loop on /stuauth (StudentLayout)
The simplest, reliable fix is to stop redirecting when the user is already on the auth route (or an auth subpath). Replace the unconditional redirect with a guarded one that checks location and hydration, and pass state so the auth page can return the user to where they intended to go.
Corrected StudentLayout example:
// StudentLayout.tsx
import React from "react";
import { Outlet, Navigate, useLocation } from "react-router-dom";
import { useAuth } from "../shared/contexts/AuthContext";
import Protected from "../shared/components/Protected";
import Sidebar from "../shared/components/Sidebar";
const StudentLayout: React.FC = () => {
const { student, hydrated } = useAuth();
const loc = useLocation();
// wait until auth is initialized (avoid premature redirects)
if (!hydrated) return null;
// don't redirect if we're already on the student auth page (or any auth subpath)
if (!student && !loc.pathname.startsWith("/stuauth")) {
// preserve where user tried to go
return <Navigate to="/stuauth" state={{ from: loc }} replace />;
}
return (
<Protected role="student">
<div className="min-h-screen flex">
<Sidebar variant="student" />
<main className="flex-1 p-4 md:p-6">
<Outlet />
</main>
</div>
</Protected>
);
};
export default StudentLayout;
Notes:
- startsWith(“/stuauth”) covers /stuauth and subpaths like /stuauth/callback. Use exact equality if you only want the literal path check.
- Passing state:
{ state: { from: loc } }preserves the original requested path so StudentAuthPage can navigate back after a successful login. - Always use
replaceon redirects from layouts that run on mount — this avoids polluting history and gives a cleaner back-button behavior.
StudentAuthPage: navigate back after login
// StudentAuthPage.tsx (pseudo)
import { useNavigate, useLocation } from "react-router-dom";
const StudentAuthPage = () => {
const navigate = useNavigate();
const location = useLocation();
const from = (location.state as any)?.from?.pathname ?? "/student";
async function onLoginSuccess() {
// perform login, set auth in context...
navigate(from, { replace: true });
}
// ...
};
For more detailed guarded-route patterns see Robin Wieruch’s guide: https://www.robinwieruch.de/react-router-authentication.
Prevent unexpected redirect from root /
If visiting / immediately redirects to /stuauth, one of these is usually true:
- StudentLayout (or a global guard) is being evaluated for
/(route config or layout ordering issue). - AuthProvider’s
hydrateddefaults totrueor your app triggers redirects before async auth finishes. - A component (Sidebar, Protected, etc.) is calling navigate unguarded.
What to do:
- Consolidate your public routes and the NotFound route under a single PublicLayout entry. Duplicate top-level PublicLayout entries can cause confusion. Example router structure:
const router = createBrowserRouter([
{
element: <PublicLayout />,
children: [
{ index: true, element: <LandingPage /> }, // '/'
{ path: "stuauth", element: <StudentAuthPage /> },
{ path: "adlogin", element: <AdminLoginPage /> },
{ path: "*", element: <NotFound /> },
],
},
{
path: "student",
element: <StudentLayout />,
children: [
// student routes
],
},
{
path: "admin",
element: <AdminLayout />,
children: [
// admin routes
],
},
]);
-
Ensure AuthProvider sets
hydratedto false while it loads stored tokens/session and only sets it true after that asynchronous work completes. In StudentLayout, returningnullwhile !hydrated prevents premature redirects. -
Audit any component that calls navigate in a useEffect. Prefer declarative redirects (return <Navigate … />) over imperative useEffect navigations for auth gating, or at least always check location and hydration before calling navigate.
-
Confirm that
/stuauthis not accidentally nested beneath/studentin your real route definitions — even a small typo or route order change can cause the auth page to be matched inside the student layout.
If you want robust server-like behavior (auth check before any UI mounts), consider the route loader approach described next.
Alternative: route loaders and Protected patterns
React Router (v6.4+) route loaders let you redirect before the element mounts. That avoids a layout mounting and then redirecting.
Example loader-based protection:
// studentLoader.ts
import { redirect } from "react-router-dom";
export async function studentLoader({ request }: any) {
// synchronous check (cookies) or fetch to /api/session
const session = await fetch("/api/session").then(r => r.json()).catch(() => null);
if (!session || session.role !== "student") {
const url = new URL(request.url);
throw redirect(`/stuauth?from=${encodeURIComponent(url.pathname)}`);
}
return null;
}
Attach to route:
{
path: "student",
loader: studentLoader,
element: <StudentLayout />,
children: [ /* ... */ ],
}
Caveats:
- Loaders can’t directly read React context — they should use cookies or an API endpoint that verifies a session.
- Loaders are excellent when you need the router to decide navigation before rendering a layout; they remove the flicker and avoid redirect loops.
If you prefer components, use a small declarative Protected wrapper that returns
function Protected({ children, role }) {
const { user, hydrated } = useAuth();
const location = useLocation();
if (!hydrated) return null;
if (!user) return <Navigate to="/stuauth" state={{ from: location }} replace />;
if (role && user.role !== role) return <Navigate to="/not-authorized" replace />;
return children;
}
This pattern is discussed in community threads and tutorials (see https://ui.dev/react-router-protected-routes-authentication).
Debugging checklist
- Confirm where StudentLayout mounts: add console.log in StudentLayout to print mount events and current location.pathname.
- Log auth provider lifecycle: console.log hydrations and the initial value of
hydrated. Make sure hydration is false until async checks finish. - Check your route tree with React Router DevTools or by temporarily rendering
useMatches()results to see which layouts are matched for/and/stuauth. - Search the codebase for programmatic navigate calls (useNavigate) used on mount — ensure they guard against redirecting when location already equals target.
- Ensure
/stuauthisn’t duplicated or nested under/studentby mistake (typos with leading slashes or wrong nesting are common). - Try the guarded-change quickly: add
!loc.pathname.startsWith('/stuauth')to StudentLayout; if that fixes the loop you’ve found the root cause. - If you use tokens in localStorage/cookies, verify the AuthProvider reads them and sets hydrated only after that read. A mistaken default
hydrated = truewill cause premature redirects. - Reproduce minimal example: reduce router to PublicLayout + StudentLayout + one child each. If the problem disappears, re-introduce pieces until it reappears — that isolates the culprit.
If you see “Maximum update depth exceeded” in the console, that confirms a repeated redirect / re-render cycle.
Sources
- https://ui.dev/react-router-protected-routes-authentication
- https://stackoverflow.com/questions/67361430/how-can-i-avoid-infinite-loops-in-my-react-router-private-routes
- https://www.reddit.com/r/reactjs/comments/1eovjpg/stuck_in_infinite_loop_with_react_router/
- https://www.robinwieruch.de/react-router-authentication/
- https://github.com/ReactTraining/react-router/issues/5003
Conclusion
In short: the infinite redirect loop arises because StudentLayout redirects to /stuauth without checking the current location or waiting for auth hydration. Fix it by keeping /stuauth in your public routes, guarding the layout-level redirect with a location check (and using state + replace), or by using route loaders so redirects happen before the layout mounts. Those changes will stop the unexpected redirect from / and eliminate the infinite redirect loop in your React Router createBrowserRouter setup.