Web

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.

1 answer 1 view

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:

tsx
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 under PublicLayout with LandingPage, which shouldn’t check student auth.
  • StudentLayout redirects to /stuauth if no student in auth context.
  • /stuauth is also under PublicLayout.

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


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:

tsx
// 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 replace on redirects from layouts that run on mount — this avoids polluting history and gives a cleaner back-button behavior.

StudentAuthPage: navigate back after login

tsx
// 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:

  1. StudentLayout (or a global guard) is being evaluated for / (route config or layout ordering issue).
  2. AuthProvider’s hydrated defaults to true or your app triggers redirects before async auth finishes.
  3. 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:
tsx
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 hydrated to false while it loads stored tokens/session and only sets it true after that asynchronous work completes. In StudentLayout, returning null while !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 /stuauth is not accidentally nested beneath /student in 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:

tsx
// 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:

tsx
{
 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 rather than using useEffect navigation:

tsx
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 /stuauth isn’t duplicated or nested under /student by 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 = true will 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


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.

Authors
Verified by moderation
Moderation
Fix React Router Infinite Redirect Loop on /stuauth & Root