AdSense Leaderboard

Google AdSense Placeholder

HEADER SLOT

React Router 7 Overview: Complete Routing Guide for SPAs

Last updated:
React Router 7 Overview: Complete Routing Guide for SPAs

Master React Router for building multi-page SPAs. Learn routing setup, navigation, nested routes, parameters, and data loading with modern best practices.

Table of Contents

# React Router 7 Overview: Complete Routing Guide for SPAs

Building a modern single-page application (SPA) without a router is like driving a car without steering—you might move forward, but you'll eventually crash. React Router has been the go-to solution for years, and for good reason. It handles URL synchronization, browser navigation, data fetching, and code splitting in ways that hand-rolled solutions simply can't match.

But here's what surprises many developers: React Router is more than just URL matching. It's a complete framework for managing application state based on the URL, orchestrating data fetching, and coordinating component lifecycles across your entire app. Mastering it unlocks possibilities you didn't know were possible.

Let's explore how to build robust, maintainable routing architectures with React Router 7.

# Table of Contents

  1. What Is React Router and Why You Need It
  2. Installation and Core Concepts
  3. Setting Up Your Router
  4. Declaring Routes
  5. Navigation Components: Link and NavLink
  6. Nested Routes and Route Composition
  7. Route Parameters and Dynamic Segments
  8. Programmatic Navigation with useNavigate
  9. Search Parameters and Query Strings
  10. Error Boundaries and Error Pages
  11. Code Splitting and Lazy Loading
  12. Data Loading with Data Loaders
  13. FAQ

# What Is React Router and Why You Need It {#what-is-router}

React Router solves a fundamental problem: how do you synchronize your application's UI with the browser's URL? Without a router, clicking a link does nothing—it just reloads the page and loses all application state.

A router like React Router does four critical things:

  1. Synchronizes URL with component rendering - When the user navigates to /products/5, React Router renders the appropriate component and passes the product ID as a parameter.

  2. Manages browser history - Back/forward buttons work without you building custom history management.

  3. Enables bookmarking and sharing - Users can share URLs that load the exact state of your application.

  4. Supports code splitting - Routes can be lazy-loaded, keeping your initial bundle small.

Without these capabilities, you're forced to either build them yourself (which is painful) or accept a degraded user experience.

# Installation and Core Concepts {#installation}

# Install React Router

bash
npm install react-router-dom

TypeScript types are included, so no separate @types package is needed.

# Core Concepts

Route - A mapping between a URL path and a component to render. When the user navigates to that path, the component appears.

Router - The component that manages routes and navigation. React Router provides several router types for different environments (browsers, servers, etc.).

Navigation - Moving between routes. Can be done via <Link>, <NavLink>, <Form>, or programmatically with useNavigate().

Outlet - A placeholder for nested route components. When a nested route matches, its component renders inside the <Outlet> of the parent route.

Loader - A function that fetches data before a route renders. This ensures data is available before the component appears.

Action - A function that handles form submissions. Similar to loaders but for mutations.

# Setting Up Your Router {#router-setup}

React Router uses createBrowserRouter() to configure routes and RouterProvider to inject the router into your app.

# TypeScript Version

typescript
import {
  createBrowserRouter,
  RouterProvider,
  Navigate,
} from 'react-router-dom';
import { ReactNode } from 'react';

// Page components
import HomePage from './pages/HomePage';
import ProductsPage from './pages/ProductsPage';
import ProductDetailPage from './pages/ProductDetailPage';
import NotFoundPage from './pages/NotFoundPage';

// Layout components
interface LayoutProps {
  children?: ReactNode;
}

function RootLayout({ children }: LayoutProps) {
  return (
    <div className="app">
      <header>
        <nav>
          {/* Navigation here */}
        </nav>
      </header>
      <main>{children}</main>
      <footer2025 My App</footer>
    </div>
  );
}

// Define routes
const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout><HomePage /></RootLayout>,
    errorElement: <NotFoundPage />,
  },
  {
    path: '/products',
    element: <ProductsPage />,
  },
  {
    path: '/products/:id',
    element: <ProductDetailPage />,
  },
  {
    path: '*',
    element: <Navigate to="/" />,
  },
]);

// Provide router to app
export default function App() {
  return <RouterProvider router={router} />;
}

# JavaScript Version

javascript
import {
  createBrowserRouter,
  RouterProvider,
  Navigate,
} from 'react-router-dom';

// Page components
import HomePage from './pages/HomePage';
import ProductsPage from './pages/ProductsPage';
import ProductDetailPage from './pages/ProductDetailPage';
import NotFoundPage from './pages/NotFoundPage';

function RootLayout({ children }) {
  return (
    <div className="app">
      <header>
        <nav>{/* Navigation */}</nav>
      </header>
      <main>{children}</main>
      <footer>© 2025 My App</footer>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout><HomePage /></RootLayout>,
    errorElement: <NotFoundPage />,
  },
  {
    path: '/products',
    element: <ProductsPage />,
  },
  {
    path: '/products/:id',
    element: <ProductDetailPage />,
  },
  {
    path: '*',
    element: <Navigate to="/" />,
  },
]);

export default function App() {
  return <RouterProvider router={router} />;
}

# Declaring Routes {#declaring-routes}

Routes are defined as plain JavaScript objects. Each route object has these key properties:

Property Purpose Required
path URL path to match (can include parameters like :id) Yes
element Component to render when path matches Yes
errorElement Component to render if an error occurs No
children Nested routes No
loader Function to fetch data before rendering No
action Function to handle form submissions No

# TypeScript Version: Advanced Route Configuration

typescript
import {
  LoaderFunctionArgs,
  ActionFunctionArgs,
  redirect,
} from 'react-router-dom';

interface Product {
  id: string;
  name: string;
  price: number;
}

// Loader functions fetch data before the route renders
const productLoader = async ({ params }: LoaderFunctionArgs) => {
  const { id } = params;
  const response = await fetch(`/api/products/${id}`);
  
  if (!response.ok) {
    throw new Response('Product not found', { status: 404 });
  }
  
  return response.json() as Promise<Product>;
};

// Action functions handle form submissions
const createProductAction = async ({ request }: ActionFunctionArgs) => {
  if (request.method === 'POST') {
    const formData = await request.formData();
    const response = await fetch('/api/products', {
      method: 'POST',
      body: formData,
    });

    if (!response.ok) {
      throw new Response('Failed to create product', { status: 400 });
    }

    const product = await response.json();
    return redirect(`/products/${product.id}`);
  }

  return null;
};

interface RouteConfig {
  path: string;
  element: JSX.Element;
  loader?: (args: LoaderFunctionArgs) => Promise<Product | null>;
  action?: (args: ActionFunctionArgs) => Promise<any>;
  errorElement?: JSX.Element;
  children?: RouteConfig[];
}

const routes: RouteConfig[] = [
  {
    path: '/products/:id',
    element: <ProductDetailPage />,
    loader: productLoader,
    errorElement: <ErrorPage />,
  },
  {
    path: '/products/new',
    element: <CreateProductPage />,
    action: createProductAction,
  },
];

# JavaScript Version

javascript
// Loader function
const productLoader = async ({ params }) => {
  const { id } = params;
  const response = await fetch(`/api/products/${id}`);
  
  if (!response.ok) {
    throw new Response('Product not found', { status: 404 });
  }
  
  return response.json();
};

// Action function
const createProductAction = async ({ request }) => {
  if (request.method === 'POST') {
    const formData = await request.formData();
    const response = await fetch('/api/products', {
      method: 'POST',
      body: formData,
    });

    if (!response.ok) {
      throw new Response('Failed to create product', { status: 400 });
    }

    const product = await response.json();
    return redirect(`/products/${product.id}`);
  }

  return null;
};

const routes = [
  {
    path: '/products/:id',
    element: <ProductDetailPage />,
    loader: productLoader,
    errorElement: <ErrorPage />,
  },
  {
    path: '/products/new',
    element: <CreateProductPage />,
    action: createProductAction,
  },
];

React Router provides two components for navigation: <Link> and <NavLink>.

<Link> - Renders an <a> tag that navigates without a full page reload. Use this for most links in your app.

<NavLink> - A special <Link> that's aware of whether its route is active. Perfect for navigation menus and breadcrumbs.

# TypeScript Version

typescript
import { Link, NavLink, useLocation } from 'react-router-dom';

interface NavigationProps {
  isOpen?: boolean;
}

export function Navigation({ isOpen }: NavigationProps) {
  const location = useLocation();

  // Determine if a link is active
  const isActive = (path: string) => location.pathname === path;

  return (
    <nav className="navbar">
      {/* Basic Link - no styling awareness */}
      <Link to="/" className="logo">
        My App
      </Link>

      {/* NavLink with className function - gets isActive prop */}
      <NavLink
        to="/products"
        className={({ isActive }) =>
          isActive ? 'nav-link active' : 'nav-link'
        }
      >
        Products
      </NavLink>

      {/* NavLink with style function */}
      <NavLink
        to="/about"
        style={({ isActive }) => ({
          color: isActive ? 'blue' : 'gray',
          textDecoration: isActive ? 'underline' : 'none',
        })}
      >
        About
      </NavLink>

      {/* Link to dynamic routes with state */}
      <Link
        to="/products/42"
        state={{ referrer: 'search' }}
        className="link"
      >
        Product 42
      </Link>
    </nav>
  );
}

# JavaScript Version

javascript
import { Link, NavLink, useLocation } from 'react-router-dom';

export function Navigation({ isOpen }) {
  const location = useLocation();

  const isActive = (path) => location.pathname === path;

  return (
    <nav className="navbar">
      <Link to="/" className="logo">
        My App
      </Link>

      <NavLink
        to="/products"
        className={({ isActive }) =>
          isActive ? 'nav-link active' : 'nav-link'
        }
      >
        Products
      </NavLink>

      <NavLink
        to="/about"
        style={({ isActive }) => ({
          color: isActive ? 'blue' : 'gray',
          textDecoration: isActive ? 'underline' : 'none',
        })}
      >
        About
      </NavLink>

      <Link
        to="/products/42"
        state={{ referrer: 'search' }}
        className="link"
      >
        Product 42
      </Link>
    </nav>
  );
}

# Nested Routes and Route Composition {#nested-routes}

Nested routes allow you to structure your URL hierarchy and UI hierarchy together. A parent route renders an <Outlet> component, and child routes render their components inside that outlet.

# TypeScript Version

typescript
import {
  createBrowserRouter,
  RouterProvider,
  Outlet,
} from 'react-router-dom';

// Parent layout component
export function AdminLayout() {
  return (
    <div className="admin-container">
      <aside className="admin-sidebar">
        <AdminMenu />
      </aside>
      <main className="admin-main">
        {/* Child routes render here */}
        <Outlet />
      </main>
    </div>
  );
}

// Admin page components
export function AdminDashboard() {
  return <h1>Admin Dashboard</h1>;
}

export function AdminUsers() {
  return <h1>Manage Users</h1>;
}

export function AdminSettings() {
  return <h1>Settings</h1>;
}

// Define nested routes
const routes = [
  {
    path: '/admin',
    element: <AdminLayout />,
    children: [
      {
        path: '', // Default nested route
        element: <AdminDashboard />,
      },
      {
        path: 'users',
        element: <AdminUsers />,
      },
      {
        path: 'settings',
        element: <AdminSettings />,
      },
    ],
  },
];

// Now these paths work:
// /admin → renders AdminLayout with AdminDashboard in Outlet
// /admin/users → renders AdminLayout with AdminUsers in Outlet
// /admin/settings → renders AdminLayout with AdminSettings in Outlet

# JavaScript Version

javascript
import { createBrowserRouter, Outlet } from 'react-router-dom';

export function AdminLayout() {
  return (
    <div className="admin-container">
      <aside className="admin-sidebar">
        <AdminMenu />
      </aside>
      <main className="admin-main">
        <Outlet />
      </main>
    </div>
  );
}

export function AdminDashboard() {
  return <h1>Admin Dashboard</h1>;
}

export function AdminUsers() {
  return <h1>Manage Users</h1>;
}

export function AdminSettings() {
  return <h1>Settings</h1>;
}

const routes = [
  {
    path: '/admin',
    element: <AdminLayout />,
    children: [
      {
        path: '',
        element: <AdminDashboard />,
      },
      {
        path: 'users',
        element: <AdminUsers />,
      },
      {
        path: 'settings',
        element: <AdminSettings />,
      },
    ],
  },
];

# Route Parameters and Dynamic Segments {#route-parameters}

Routes can have dynamic segments (like :id) that capture values from the URL. Access these values with the useParams() hook.

# TypeScript Version

typescript
import { useParams, useLoaderData } from 'react-router-dom';

interface Product {
  id: string;
  name: string;
  price: number;
  description: string;
}

interface Params {
  id?: string;
}

// Loader function receives params
const productLoader = async ({ params }: { params: Params }) => {
  const { id } = params;
  const response = await fetch(`/api/products/${id}`);
  return response.json() as Promise<Product>;
};

// Component accesses params and loader data
export function ProductDetailPage() {
  const { id } = useParams<Params>();
  const product = useLoaderData() as Product;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Price: ${product.price}</p>
      <p>ID: {id}</p>
      <p>{product.description}</p>
    </div>
  );
}

// Route definition
const productRoute = {
  path: '/products/:id',
  element: <ProductDetailPage />,
  loader: productLoader,
};

// Possible URLs:
// /products/123 → id = "123"
// /products/abc → id = "abc"

// Multiple parameters
const reviewRoute = {
  path: '/products/:productId/reviews/:reviewId',
  element: <ReviewPage />,
  loader: ({ params }) => {
    const { productId, reviewId } = params;
    return fetch(`/api/products/${productId}/reviews/${reviewId}`).then(r => r.json());
  },
};

# JavaScript Version

javascript
import { useParams, useLoaderData } from 'react-router-dom';

const productLoader = async ({ params }) => {
  const { id } = params;
  const response = await fetch(`/api/products/${id}`);
  return response.json();
};

export function ProductDetailPage() {
  const { id } = useParams();
  const product = useLoaderData();

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Price: ${product.price}</p>
      <p>ID: {id}</p>
      <p>{product.description}</p>
    </div>
  );
}

const productRoute = {
  path: '/products/:id',
  element: <ProductDetailPage />,
  loader: productLoader,
};

# Programmatic Navigation with useNavigate {#programmatic-navigation}

Sometimes you need to navigate programmatically (not via a link click). Use the useNavigate() hook.

# TypeScript Version

typescript
import { useNavigate } from 'react-router-dom';

interface LoginPageProps {
  onSuccess?: () => void;
}

export function LoginPage({ onSuccess }: LoginPageProps) {
  const navigate = useNavigate();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState<string | null>(null);

  const handleLogin = async (e: React.FormEvent) => {
    e.preventDefault();

    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        setError('Invalid credentials');
        return;
      }

      onSuccess?.();

      // Navigate to dashboard after successful login
      navigate('/dashboard', {
        replace: true, // Replace history entry so back button doesn't go to login
        state: { loginSuccess: true },
      });
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Login failed');
    }
  };

  return (
    <form onSubmit={handleLogin}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      {error && <p className="error">{error}</p>}
      <button type="submit">Login</button>
    </form>
  );
}

// Navigation methods
export function NavigationExamples() {
  const navigate = useNavigate();

  return (
    <div>
      {/* Navigate forward */}
      <button onClick={() => navigate('/products')}>
        Go to Products
      </button>

      {/* Navigate with state */}
      <button
        onClick={() =>
          navigate('/checkout', {
            state: { cartTotal: 99.99 },
          })
        }
      >
        Checkout
      </button>

      {/* Navigate relative to current location */}
      <button onClick={() => navigate('../')}>
        Go to parent
      </button>

      {/* Navigate backward in history */}
      <button onClick={() => navigate(-1)}>
        Back
      </button>

      {/* Replace current history entry */}
      <button
        onClick={() =>
          navigate('/home', { replace: true })
        }
      >
        Go Home (replace history)
      </button>
    </div>
  );
}

# JavaScript Version

javascript
import { useNavigate } from 'react-router-dom';
import { useState } from 'react';

export function LoginPage({ onSuccess }) {
  const navigate = useNavigate();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState(null);

  const handleLogin = async (e) => {
    e.preventDefault();

    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ email, password }),
      });

      if (!response.ok) {
        setError('Invalid credentials');
        return;
      }

      onSuccess?.();
      navigate('/dashboard', {
        replace: true,
        state: { loginSuccess: true },
      });
    } catch (err) {
      setError(err.message || 'Login failed');
    }
  };

  return (
    <form onSubmit={handleLogin}>
      <input
        type="email"
        value={email}
        onChange={(e) => setEmail(e.target.value)}
        placeholder="Email"
      />
      <input
        type="password"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
        placeholder="Password"
      />
      {error && <p className="error">{error}</p>}
      <button type="submit">Login</button>
    </form>
  );
}

# Search Parameters and Query Strings {#search-parameters}

Search parameters (query strings) let you store UI state in the URL without defining new routes. Access them with useSearchParams().

# TypeScript Version

typescript
import { useSearchParams } from 'react-router-dom';

interface Filter {
  category?: string;
  minPrice?: number;
  maxPrice?: number;
  sort?: 'name' | 'price' | 'rating';
}

export function ProductsPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  // Extract search params
  const category = searchParams.get('category') || '';
  const minPrice = searchParams.get('minPrice')
    ? Number(searchParams.get('minPrice'))
    : undefined;
  const sort = searchParams.get('sort') || 'name';

  // Update search params
  const handleFilterChange = (filters: Filter) => {
    const params = new URLSearchParams();

    if (filters.category) params.set('category', filters.category);
    if (filters.minPrice) params.set('minPrice', String(filters.minPrice));
    if (filters.maxPrice) params.set('maxPrice', String(filters.maxPrice));
    if (filters.sort) params.set('sort', filters.sort);

    setSearchParams(params);
  };

  const handleClearFilters = () => {
    setSearchParams({});
  };

  return (
    <div>
      <h1>Products</h1>

      <aside className="filters">
        <input
          type="text"
          placeholder="Search by category"
          value={category}
          onChange={(e) =>
            handleFilterChange({ category: e.target.value, sort: sort as any })
          }
        />
        <select
          value={sort}
          onChange={(e) =>
            handleFilterChange({
              category,
              sort: e.target.value as any,
            })
          }
        >
          <option value="name">Sort by Name</option>
          <option value="price">Sort by Price</option>
          <option value="rating">Sort by Rating</option>
        </select>
        <button onClick={handleClearFilters}>Clear Filters</button>
      </aside>

      <main className="products">
        {/* Products filtered by category, minPrice, maxPrice, sort */}
        <ProductList
          category={category}
          minPrice={minPrice}
          sort={sort}
        />
      </main>
    </div>
  );
}

# JavaScript Version

javascript
import { useSearchParams } from 'react-router-dom';

export function ProductsPage() {
  const [searchParams, setSearchParams] = useSearchParams();

  const category = searchParams.get('category') || '';
  const minPrice = searchParams.get('minPrice')
    ? Number(searchParams.get('minPrice'))
    : undefined;
  const sort = searchParams.get('sort') || 'name';

  const handleFilterChange = (filters) => {
    const params = new URLSearchParams();

    if (filters.category) params.set('category', filters.category);
    if (filters.minPrice) params.set('minPrice', String(filters.minPrice));
    if (filters.maxPrice) params.set('maxPrice', String(filters.maxPrice));
    if (filters.sort) params.set('sort', filters.sort);

    setSearchParams(params);
  };

  const handleClearFilters = () => {
    setSearchParams({});
  };

  return (
    <div>
      <h1>Products</h1>

      <aside className="filters">
        <input
          type="text"
          placeholder="Search by category"
          value={category}
          onChange={(e) =>
            handleFilterChange({ category: e.target.value, sort })
          }
        />
        <select
          value={sort}
          onChange={(e) =>
            handleFilterChange({ category, sort: e.target.value })
          }
        >
          <option value="name">Sort by Name</option>
          <option value="price">Sort by Price</option>
          <option value="rating">Sort by Rating</option>
        </select>
        <button onClick={handleClearFilters}>Clear Filters</button>
      </aside>

      <main className="products">
        <ProductList
          category={category}
          minPrice={minPrice}
          sort={sort}
        />
      </main>
    </div>
  );
}

# Error Boundaries and Error Pages {#error-handling}

React Router handles errors elegantly with errorElement and custom error boundaries.

# TypeScript Version

typescript
import {
  isRouteErrorResponse,
  useRouteError,
} from 'react-router-dom';

export function ErrorPage() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div className="error-container">
        <h1>{error.status}</h1>
        <p>{error.statusText || 'An error occurred'}</p>
        {error.data?.message && (
          <p className="error-detail">{error.data.message}</p>
        )}
      </div>
    );
  }

  if (error instanceof Error) {
    return (
      <div className="error-container">
        <h1>Error</h1>
        <p>{error.message}</p>
        {import.meta.env.DEV && (
          <details>
            <summary>Stack trace (dev only)</summary>
            <pre>{error.stack}</pre>
          </details>
        )}
      </div>
    );
  }

  return (
    <div className="error-container">
      <h1>Unknown Error</h1>
      <p>Something went wrong. Please try again.</p>
    </div>
  );
}

// Use in routes
const routes = [
  {
    path: '/',
    element: <Home />,
    errorElement: <ErrorPage />,
  },
  {
    path: '/products/:id',
    element: <ProductDetailPage />,
    loader: ({ params }) => {
      if (!params.id) {
        throw new Response('Product ID is required', { status: 400 });
      }
      return fetch(`/api/products/${params.id}`);
    },
    errorElement: <ErrorPage />,
  },
];

# JavaScript Version

javascript
import {
  isRouteErrorResponse,
  useRouteError,
} from 'react-router-dom';

export function ErrorPage() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    return (
      <div className="error-container">
        <h1>{error.status}</h1>
        <p>{error.statusText || 'An error occurred'}</p>
        {error.data?.message && (
          <p className="error-detail">{error.data.message}</p>
        )}
      </div>
    );
  }

  if (error instanceof Error) {
    return (
      <div className="error-container">
        <h1>Error</h1>
        <p>{error.message}</p>
      </div>
    );
  }

  return (
    <div className="error-container">
      <h1>Unknown Error</h1>
      <p>Something went wrong. Please try again.</p>
    </div>
  );
}

# Code Splitting and Lazy Loading {#lazy-loading}

Load route components only when needed using React.lazy() and Suspense. This dramatically reduces your initial bundle size.

# TypeScript Version

typescript
import {
  lazy,
  Suspense,
  ComponentType,
  ReactNode,
} from 'react';
import { createBrowserRouter } from 'react-router-dom';

// Lazy-loaded page components
const HomePage = lazy(() => import('./pages/HomePage'));
const AdminPage = lazy(() => import('./pages/AdminPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));

// Loading fallback
interface LoadingProps {
  message?: string;
}

function LoadingFallback({ message = 'Loading...' }: LoadingProps) {
  return (
    <div className="loading-container">
      <div className="spinner" />
      <p>{message}</p>
    </div>
  );
}

// Wrapper to handle Suspense
interface LazyRouteProps {
  Component: ComponentType;
  loadingMessage?: string;
}

function LazyRoute({ Component, loadingMessage }: LazyRouteProps) {
  return (
    <Suspense fallback={<LoadingFallback message={loadingMessage} />}>
      <Component />
    </Suspense>
  );
}

// Define routes with lazy loading
const routes = [
  {
    path: '/',
    element: <LazyRoute Component={HomePage} />,
  },
  {
    path: '/admin',
    element: <LazyRoute Component={AdminPage} loadingMessage="Loading admin..." />,
  },
  {
    path: '/settings',
    element: <LazyRoute Component={SettingsPage} />,
  },
];

export const router = createBrowserRouter(routes);

# JavaScript Version

javascript
import { lazy, Suspense } from 'react';
import { createBrowserRouter } from 'react-router-dom';

const HomePage = lazy(() => import('./pages/HomePage'));
const AdminPage = lazy(() => import('./pages/AdminPage'));
const SettingsPage = lazy(() => import('./pages/SettingsPage'));

function LoadingFallback({ message = 'Loading...' }) {
  return (
    <div className="loading-container">
      <div className="spinner" />
      <p>{message}</p>
    </div>
  );
}

function LazyRoute({ Component, loadingMessage }) {
  return (
    <Suspense fallback={<LoadingFallback message={loadingMessage} />}>
      <Component />
    </Suspense>
  );
}

const routes = [
  {
    path: '/',
    element: <LazyRoute Component={HomePage} />,
  },
  {
    path: '/admin',
    element: <LazyRoute Component={AdminPage} loadingMessage="Loading admin..." />,
  },
  {
    path: '/settings',
    element: <LazyRoute Component={SettingsPage} />,
  },
];

export const router = createBrowserRouter(routes);

# Data Loading with Data Loaders {#data-loaders}

Data loaders fetch data before a route renders. This prevents "loading spinners" in your components—the data is already there when the component mounts.

# TypeScript Version

typescript
import {
  LoaderFunctionArgs,
  useLoaderData,
  defer,
} from 'react-router-dom';
import { Suspense } from 'react';
import { Await } from 'react-router-dom';

interface Product {
  id: string;
  name: string;
  price: number;
}

interface Reviews {
  id: string;
  rating: number;
  text: string;
}

// Eager loader (waits for all data)
const productLoader = async ({ params }: LoaderFunctionArgs) => {
  const productRes = fetch(`/api/products/${params.id}`);
  const reviewsRes = fetch(`/api/products/${params.id}/reviews`);

  const [productData, reviewsData] = await Promise.all([
    productRes,
    reviewsRes,
  ]);

  return {
    product: (await productData.json()) as Product,
    reviews: (await reviewsData.json()) as Reviews[],
  };
};

// Deferred loader (renders page while loading data)
const productLoaderDeferred = async ({
  params,
}: LoaderFunctionArgs) => {
  const productPromise = fetch(`/api/products/${params.id}`).then(
    (r) => r.json()
  );
  const reviewsPromise = fetch(
    `/api/products/${params.id}/reviews`
  ).then((r) => r.json());

  return defer({
    product: productPromise,
    reviews: reviewsPromise,
  });
};

interface LoadedData {
  product: Product;
  reviews: Reviews[];
}

// Component using eager loader (has data immediately)
export function ProductPage() {
  const { product, reviews } = useLoaderData() as LoadedData;

  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <h2>Reviews</h2>
      <ul>
        {reviews.map((review) => (
          <li key={review.id}>{review.text}</li>
        ))}
      </ul>
    </div>
  );
}

// Component using deferred loader (renders while loading)
export function ProductPageDeferred() {
  const { product, reviews } = useLoaderData() as any;

  return (
    <div>
      <Suspense fallback={<h1>Loading product...</h1>}>
        <Await resolve={product}>
          {(resolvedProduct: Product) => (
            <h1>{resolvedProduct.name}</h1>
          )}
        </Await>
      </Suspense>

      <h2>Reviews</h2>
      <Suspense fallback={<p>Loading reviews...</p>}>
        <Await resolve={reviews}>
          {(resolvedReviews: Reviews[]) => (
            <ul>
              {resolvedReviews.map((review) => (
                <li key={review.id}>{review.text}</li>
              ))}
            </ul>
          )}
        </Await>
      </Suspense>
    </div>
  );
}

// Route definitions
const routes = [
  {
    path: '/products/:id',
    element: <ProductPage />,
    loader: productLoader,
  },
  {
    path: '/products/:id/deferred',
    element: <ProductPageDeferred />,
    loader: productLoaderDeferred,
  },
];

# JavaScript Version

javascript
import { useLoaderData, defer, Await } from 'react-router-dom';
import { Suspense } from 'react';

const productLoader = async ({ params }) => {
  const productRes = fetch(`/api/products/${params.id}`);
  const reviewsRes = fetch(`/api/products/${params.id}/reviews`);

  const [productData, reviewsData] = await Promise.all([
    productRes,
    reviewsRes,
  ]);

  return {
    product: await productData.json(),
    reviews: await reviewsData.json(),
  };
};

const productLoaderDeferred = async ({ params }) => {
  const productPromise = fetch(`/api/products/${params.id}`).then(
    (r) => r.json()
  );
  const reviewsPromise = fetch(
    `/api/products/${params.id}/reviews`
  ).then((r) => r.json());

  return defer({
    product: productPromise,
    reviews: reviewsPromise,
  });
};

export function ProductPage() {
  const { product, reviews } = useLoaderData();

  return (
    <div>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <h2>Reviews</h2>
      <ul>
        {reviews.map((review) => (
          <li key={review.id}>{review.text}</li>
        ))}
      </ul>
    </div>
  );
}

export function ProductPageDeferred() {
  const { product, reviews } = useLoaderData();

  return (
    <div>
      <Suspense fallback={<h1>Loading product...</h1>}>
        <Await resolve={product}>
          {(resolvedProduct) => <h1>{resolvedProduct.name}</h1>}
        </Await>
      </Suspense>

      <h2>Reviews</h2>
      <Suspense fallback={<p>Loading reviews...</p>}>
        <Await resolve={reviews}>
          {(resolvedReviews) => (
            <ul>
              {resolvedReviews.map((review) => (
                <li key={review.id}>{review.text}</li>
              ))}
            </ul>
          )}
        </Await>
      </Suspense>
    </div>
  );
}

# Practical Application: Building a complete e-commerce routing system

Here's a real-world example combining nested routes, parameters, loaders, and lazy loading:

# TypeScript Version

typescript
import {
  createBrowserRouter,
  RouterProvider,
  Outlet,
  LoaderFunctionArgs,
} from 'react-router-dom';
import { lazy, Suspense } from 'react';

interface Product {
  id: string;
  name: string;
  price: number;
}

interface LoadingProps {
  message?: string;
}

function LoadingFallback({ message = 'Loading...' }: LoadingProps) {
  return <div className="loading">{message}</div>;
}

// Lazy load pages
const HomePage = lazy(() => import('./pages/HomePage'));
const ProductsPage = lazy(() => import('./pages/ProductsPage'));
const ProductDetailPage = lazy(() => import('./pages/ProductDetailPage'));
const CartPage = lazy(() => import('./pages/CartPage'));
const CheckoutPage = lazy(() => import('./pages/CheckoutPage'));
const OrderSuccessPage = lazy(() => import('./pages/OrderSuccessPage'));

// Layout components
export function RootLayout() {
  return (
    <div className="app">
      <header className="app-header">
        <nav>
          <NavMenu />
        </nav>
      </header>
      <main className="app-main">
        <Suspense fallback={<LoadingFallback />}>
          <Outlet />
        </Suspense>
      </main>
      <footer2025 E-commerce Store</footer>
    </div>
  );
}

// Loaders
const productLoader = async ({ params }: LoaderFunctionArgs) => {
  const response = await fetch(`/api/products/${params.id}`);
  if (!response.ok) {
    throw new Response('Product not found', { status: 404 });
  }
  return response.json() as Promise<Product>;
};

// Router configuration
export const router = createBrowserRouter([
  {
    path: '/',
    element: <RootLayout />,
    errorElement: <ErrorPage />,
    children: [
      {
        path: '',
        element: (
          <Suspense fallback={<LoadingFallback message="Loading home..." />}>
            <HomePage />
          </Suspense>
        ),
      },
      {
        path: 'products',
        element: (
          <Suspense fallback={<LoadingFallback message="Loading products..." />}>
            <ProductsPage />
          </Suspense>
        ),
      },
      {
        path: 'products/:id',
        element: (
          <Suspense fallback={<LoadingFallback message="Loading product..." />}>
            <ProductDetailPage />
          </Suspense>
        ),
        loader: productLoader,
      },
      {
        path: 'cart',
        element: (
          <Suspense fallback={<LoadingFallback message="Loading cart..." />}>
            <CartPage />
          </Suspense>
        ),
      },
      {
        path: 'checkout',
        element: (
          <Suspense fallback={<LoadingFallback message="Processing..." />}>
            <CheckoutPage />
          </Suspense>
        ),
      },
      {
        path: 'order-success/:orderId',
        element: (
          <Suspense fallback={<LoadingFallback message="Confirming order..." />}>
            <OrderSuccessPage />
          </Suspense>
        ),
      },
    ],
  },
]);

export default function App() {
  return <RouterProvider router={router} />;
}

Performance note: This setup uses lazy loading for all pages, nested routes for consistent layout, loaders for data fetching, and error handling for a complete, production-ready routing system.

# FAQ

# Q: What's the difference between useNavigate and <Link>?

A: <Link> is for navigation in JSX (like in templates). useNavigate() is a hook for programmatic navigation (e.g., after a form submission). Use <Link> most of the time; use useNavigate() when you need to navigate conditionally or after an async operation.

# Q: How do I pass data between routes?

A: You have three options:

  1. URL state: Use route parameters or search params
  2. Navigation state: Pass state prop to <Link> or options to useNavigate()
  3. Global state: Use Context, Redux, or Zustand for shared state

Route parameters are best for data that should be bookmarkable. Navigation state is for transient data.

# Q: Should I use loaders or useEffect for data fetching?

A: Prefer loaders. They fetch data before rendering, eliminating loading states in components. Use useEffect only when data isn't part of route configuration (like polling or real-time updates).

# Q: How do I protect routes that require authentication?

A: Create a wrapper component that checks auth status:

typescript
export function ProtectedRoute({ children, isAuthenticated }: any) {
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  return children;
}

// Use in routes
{
  path: '/admin',
  element: <ProtectedRoute isAuthenticated={user !== null}><AdminPage /></ProtectedRoute>
}

# Q: Can I use React Router without a router?

A: Technically yes, but you lose browser integration. Without React Router, back buttons don't work, bookmarks don't work, and sharing URLs fails. For any multi-page app, use a router.

# Q: What's the best way to handle 404 pages?

A: Add a catch-all route at the end:

typescript
{
  path: '*',
  element: <NotFoundPage />
}

Ready to build your routing system? Share your routing challenges in the comments. What router patterns have solved your navigation problems?

Sponsored Content

Google AdSense Placeholder

CONTENT SLOT

Sponsored

Google AdSense Placeholder

FOOTER SLOT