Router Guards, Lazy Routes, and Suspense

Introduction

Protected routes redirect unauthenticated users to login. Lazy loading splits JavaScript so users download only the code for pages they visit. Suspense shows fallbacks while lazy chunks load. This chapter covers auth wrappers, React.lazy, loaders, and scroll restoration.

Prerequisites

Protected Route Wrapper

src/components/ProtectedRoute.tsx:

Router:

tsx
{
  path: "dashboard",
  element: (
    <ProtectedRoute isAuthenticated={Boolean(token)}>
      <DashboardPage />
    </ProtectedRoute>
  ),
},

With Zustand later:

tsx
function ProtectedRoute({ children }: { children: React.ReactNode }) {
  const token = useAuthStore((s) => s.token);
  // ...
}

Compare Vue navigation guards.

Login Redirect Back

LoginPage.tsx:

Lazy-Loaded Pages

Or wrap once at layout:

tsx
// RootLayout.tsx
<Suspense fallback={<PageSkeleton />}>
  <Outlet />
</Suspense>

Code explanation:

  • import() creates a separate chunk in dist/assets/
  • Suspense must wrap lazy components

Nested Admin Layout

tsx
{
  path: "/",
  element: <ProtectedRoute isAuthenticated={isLoggedIn}><AdminLayout /></ProtectedRoute>,
  children: [
    { index: true, element: <Navigate to="dashboard" replace /> },
    { path: "dashboard", element: <DashboardPage /> },
    { path: "settings", element: <SettingsPage /> },
  ],
},

AdminLayout.tsx:

Full project: Admin Dashboard With Auth.

Loader (Data Router)

Prefetch data before render:

tsx
async function postLoader({ params }: { params: { slug?: string } }) {
  const res = await fetch(`/api/v1/posts/${params.slug}`);
  if (!res.ok) throw new Response("Not Found", { status: 404 });
  return res.json();
}
 
{
  path: "posts/:slug",
  element: <PostDetailPage />,
  loader: postLoader,
},

PostDetailPage.tsx:

tsx
import { useLoaderData } from "react-router-dom";
 
export function PostDetailPage() {
  const post = useLoaderData() as Post;
  return <h1>{post.title}</h1>;
}

Alternative: fetch in usePost(slug) custom hook—Custom Hooks.

Error Boundaries (Route Level)

tsx
{
  path: "posts/:slug",
  element: <PostDetailPage />,
  loader: postLoader,
  errorElement: <PostErrorPage />,
},

Scroll Restoration

tsx
import { ScrollRestoration } from "react-router-dom";
 
// In root layout
<ScrollRestoration />

Or manual window.scrollTo(0, 0) in useEffect on pathname change.

Auth Guard Checklist

  • /login public; /dashboard protected
  • Redirect with ? or state.from for return URL
  • Logged-in user visiting /login → redirect home
  • 401 from API clears token and navigates to login

FAQ

beforeEach equivalent?

No global hook—use ProtectedRoute, layout wrappers, or loader redirects.

Lazy + TypeScript default export?

tsx
const Page = lazy(() => import("./Page"));  // Page must export default

Named export:

tsx
lazy(() => import("./Page").then((m) => ({ default: m.Page })));

Suspense with data fetching?

React 19 use and frameworks like TanStack Query—start with hook + loading state in chapter 16.

Chunk load failed on deploy?

Users on old tabs after deploy—catch dynamic import error and prompt refresh.

Next chapter?

Zustand State Management.