React Router

1. πŸ“– Introduction

Almost every real-world React application needs more than one "page" β€” a home screen, a details view, a settings panel, and so on. React Router is the de-facto standard library for handling this kind of client-side navigation. This tutorial covers everything from basic setup to advanced data loading, protected routes, and performance techniques, using the modern RouterProvider-based API introduced in React Router v6.4+.

Information

Code examples in this guide assume React Router v6.4+, which introduced data routers, loaders, and actions. Older BrowserRouter-only patterns are also covered for compatibility.

2. ❓ What is React Router?

React Router is a routing library for React that maps URL paths to components, enabling client-side navigation without full page reloads. It intercepts clicks on <Link> elements and browser history events, then swaps out rendered components based on the current URL.

3. πŸ€” Why Use React Router?

  • Single-page app navigation without full-page reloads, keeping app state intact
  • Deep linking β€” users can bookmark or share a URL to a specific view
  • Built-in support for data loading, error handling, and code splitting per route
  • A large, mature ecosystem with strong TS support and community adoption

4. πŸ“¦ Installing React Router

terminal

npm install react-router-dom

Note

For React Native projects, install react-router-native instead β€” the core routing API is nearly identical.

5. 🌐 BrowserRouter

<BrowserRouter> is the classic way to enable routing, using the HTTP History API to keep the UI in sync with the URL. It's simple to set up but doesn't support the newer data-loading features.

main.jsx

import { BrowserRouter, Routes, Route } from "react-router-dom";

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </BrowserRouter>
  );
}

6. πŸ—οΈ RouterProvider

The modern, recommended approach uses createBrowserRouter to define routes as a configuration object, then renders them via <RouterProvider>. This unlocks loaders, actions, and better error handling.

main.jsx

import { createBrowserRouter, RouterProvider } from "react-router-dom";

const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "/about", element: <About /> },
]);

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

7. βš™οΈ Route Configuration

Routes can be defined either declaratively with <Routes>/<Route> components, or as a configuration object passed to createBrowserRouter. Both describe the same underlying route tree.

DeclarativeRoutes.jsx

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/products" element={<Products />} />
</Routes>

ObjectRoutes.jsx

const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "/products", element: <Products /> },
]);

8. πŸ—ΊοΈ Routes

<Routes> is the container component that scans its child <Route> elements and renders whichever one's path matches the current URL.

RoutesExample.jsx

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/contact" element={<Contact />} />
  <Route path="*" element={<NotFound />} />
</Routes>

9. πŸ›£οΈ Route

Each <Route> maps a path pattern to an element. Paths can be static (/about), dynamic (/users/:id), or wildcard (*) for catch-all matching.

RouteExample.jsx

<Route path="/users/:userId" element={<UserProfile />} />

10. πŸ”— Link

<Link> renders an accessible <a> tag but intercepts the click to perform client-side navigation instead of a full page reload.

LinkExample.jsx

import { Link } from "react-router-dom";

function Nav() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
    </nav>
  );
}

11. 🧭 NavLink

<NavLink> is a special version of <Link> that automatically knows whether it's "active" β€” matching the current URL β€” and exposes this via a function-as-child or className callback.

NavLinkExample.jsx

<NavLink
  to="/about"
  className={({ isActive }) => (isActive ? "nav-link active" : "nav-link")}
>
  About
</NavLink>

12. ➑️ Navigate

<Navigate> is a component that performs an imperative redirect as soon as it renders β€” useful for conditional redirects directly inside JSX.

NavigateExample.jsx

function LoginRedirect({ isAuthenticated }) {
  if (!isAuthenticated) {
    return <Navigate to="/login" replace />;
  }
  return <Dashboard />;
}

13. πŸͺ useNavigate

useNavigate returns a function for programmatic navigation β€” for example, redirecting after a successful form submission.

UseNavigateExample.jsx

import { useNavigate } from "react-router-dom";

function LoginForm() {
  const navigate = useNavigate();

  const handleSubmit = async (e) => {
    e.preventDefault();
    await login();
    navigate("/dashboard");
  };

  return <form onSubmit={handleSubmit}>{/* fields */}</form>;
}

14. πŸͺ† Nested Routes

Nested routes mirror nested UI β€” a parent route renders shared layout, while child routes render inside it via an <Outlet> (see Section 26).

NestedRoutes.jsx

<Routes>
  <Route path="/dashboard" element={<DashboardLayout />}>
    <Route path="stats" element={<Stats />} />
    <Route path="settings" element={<Settings />} />
  </Route>
</Routes>
/dashboard (DashboardLayout)
/dashboard/stats (Stats)
/dashboard/settings (Settings)

15. πŸ›οΈ Layout Routes

A layout route has no path of its own β€” it exists purely to wrap child routes in shared UI, like a header, sidebar, or footer.

LayoutRoute.jsx

<Route element={<AppLayout />}>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
</Route>

16. πŸ“ Index Routes

An index route renders in place of its parent's <Outlet> when no child path is specified β€” essentially the "default" child view.

IndexRoute.jsx

<Route path="/dashboard" element={<DashboardLayout />}>
  <Route index element={<DashboardHome />} />
  <Route path="settings" element={<Settings />} />
</Route>

17. πŸ”€ Dynamic Routes

Dynamic routes use a colon-prefixed segment (like :id) to match variable portions of a URL, such as a specific user or product.

DynamicRoute.jsx

<Route path="/products/:productId" element={<ProductDetail />} />

18. πŸ”’ Route Parameters

Route parameters are the named, dynamic segments within a path β€” like productId in /products/:productId β€” and are read inside the matched component using useParams (see Section 21).

19. ❔ Query Parameters

Query parameters appear after a ? in the URL (e.g. ?sort=price&page=2) and are typically used for filters, sorting, or pagination that shouldn't affect route matching.

20. πŸ” URL Search Parameters

React Router represents query parameters using the standard URLSearchParams web API, accessible via the useSearchParams hook.

21. πŸͺ useParams

useParams returns an object containing the dynamic segments matched by the current route.

UseParamsExample.jsx

import { useParams } from "react-router-dom";

function ProductDetail() {
  const { productId } = useParams();
  return <h1>Product #{productId}</h1>;
}

22. πŸͺ useSearchParams

useSearchParams works like useState for the URL's query string β€” reading and updating parameters causes the URL (and history) to update accordingly.

UseSearchParamsExample.jsx

import { useSearchParams } from "react-router-dom";

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();
  const sort = searchParams.get("sort") ?? "newest";

  return (
    <select value={sort} onChange={(e) => setSearchParams({ sort: e.target.value })}>
      <option value="newest">Newest</option>
      <option value="price">Price</option>
    </select>
  );
}

23. πŸͺ useLocation

useLocation returns the current location object β€” including pathname, search, hash, and any state passed during navigation.

UseLocationExample.jsx

import { useLocation } from "react-router-dom";

function PageTracker() {
  const location = useLocation();

  useEffect(() => {
    trackPageView(location.pathname);
  }, [location]);

  return null;
}

24. πŸͺ useMatch

useMatch checks whether a given path pattern matches the current URL, returning match details (like params) or null.

UseMatchExample.jsx

import { useMatch } from "react-router-dom";

function Breadcrumb() {
  const match = useMatch("/products/:productId");
  return match ? <span>Viewing product {match.params.productId}</span> : null;
}

25. πŸͺ useMatches

useMatches (data router only) returns all matched routes for the current URL, including their loader data β€” commonly used to build dynamic breadcrumb trails.

UseMatchesExample.jsx

import { useMatches } from "react-router-dom";

function Breadcrumbs() {
  const matches = useMatches();
  return (
    <nav>
      {matches.filter((m) => m.handle?.crumb).map((m) => m.handle.crumb(m.data))}
    </nav>
  );
}

26. πŸ”² Outlet

<Outlet> marks the spot within a parent (layout) route's JSX where the matched child route should render.

OutletExample.jsx

import { Outlet, Link } from "react-router-dom";

function DashboardLayout() {
  return (
    <div>
      <nav>
        <Link to="stats">Stats</Link>
        <Link to="settings">Settings</Link>
      </nav>
      <main>
        <Outlet />
      </main>
    </div>
  );
}

27. πŸ” Protected Routes

A protected route checks a condition β€” like authentication β€” before rendering its children, redirecting unauthorized users elsewhere.

ProtectedRoute.jsx

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

// Usage
<Route
  path="/dashboard"
  element={
    <ProtectedRoute isAuthenticated={isLoggedIn}>
      <Dashboard />
    </ProtectedRoute>
  }
/>

28. πŸ”‘ Authentication Routing

A common pattern wraps groups of protected routes in a single layout-style guard, rather than repeating the check on every route individually.

AuthLayout.jsx

function RequireAuth() {
  const { isAuthenticated } = useAuth();
  const location = useLocation();

  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return <Outlet />;
}

// Route config
<Route element={<RequireAuth />}>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/profile" element={<Profile />} />
</Route>

Tip

Passing state={{ from: location }} lets your login page redirect the user back to the page they originally tried to visit.

29. 🎭 Role-Based Routing

Beyond simple authentication, some routes should only be accessible to users with a specific role (e.g. admin). Extend the guard component to check both authentication and authorization.

RequireRole.jsx

function RequireRole({ role, children }) {
  const { user } = useAuth();

  if (!user) return <Navigate to="/login" replace />;
  if (user.role !== role) return <Navigate to="/unauthorized" replace />;

  return children;
}

30. πŸ’€ Lazy Loading Routes

Combining React.lazy with <Suspense> lets each route's code be downloaded only when the user actually navigates to it, reducing the initial bundle size.

LazyRoutes.jsx

import { lazy, Suspense } from "react";

const Settings = lazy(() => import("./Settings"));

<Route
  path="/settings"
  element={
    <Suspense fallback={<Spinner />}>
      <Settings />
    </Suspense>
  }
/>

31. πŸ“₯ Route Loaders

A loader function fetches data before a route renders, so the component can access it synchronously via useLoaderData β€” no more manual useEffect + loading state boilerplate.

RouteLoader.jsx

const router = createBrowserRouter([
  {
    path: "/products/:productId",
    element: <ProductDetail />,
    loader: async ({ params }) => {
      const res = await fetch(`/api/products/${params.productId}`);
      return res.json();
    },
  },
]);

function ProductDetail() {
  const product = useLoaderData();
  return <h1>{product.name}</h1>;
}

32. πŸ“ Route Actions

An action function handles data mutations β€” typically form submissions β€” triggered via a router-aware <Form> component.

RouteAction.jsx

const router = createBrowserRouter([
  {
    path: "/products/new",
    element: <NewProduct />,
    action: async ({ request }) => {
      const formData = await request.formData();
      await createProduct(Object.fromEntries(formData));
      return redirect("/products");
    },
  },
]);

function NewProduct() {
  return (
    <Form method="post">
      <input name="name" />
      <button type="submit">Create</button>
    </Form>
  );
}

33. ⏳ Deferred Data

The defer utility lets a loader return immediately for critical data while slower data streams in later, rendered with <Await> and <Suspense>.

DeferredData.jsx

loader: async () => {
  return defer({
    critical: await fetchCriticalData(),
    slowStats: fetchSlowStats(), // not awaited
  });
};

function Page() {
  const { critical, slowStats } = useLoaderData();

  return (
    <>
      <CriticalView data={critical} />
      <Suspense fallback={<Spinner />}>
        <Await resolve={slowStats}>
          {(stats) => <StatsView data={stats} />}
        </Await>
      </Suspense>
    </>
  );
}

34. 🚧 Error Boundaries

Each route can define an errorElement that renders if the route's loader, action, or rendering throws β€” replacing the entire app crashing with a graceful fallback.

ErrorBoundary.jsx

{
  path: "/products/:productId",
  element: <ProductDetail />,
  errorElement: <ProductError />,
  loader: productLoader,
}

function ProductError() {
  const error = useRouteError();
  return <p>Something went wrong: {error.message}</p>;
}

35. 🧯 Error Pages

A well-designed error page should explain what went wrong in plain language and offer a way back β€” such as a link to the homepage.

ErrorPage.jsx

function ErrorPage() {
  const error = useRouteError();
  return (
    <div>
      <h1>Oops!</h1>
      <p>{error.statusText || error.message}</p>
      <Link to="/">Go back home</Link>
    </div>
  );
}

36. πŸ•³οΈ Not Found (404) Pages

A wildcard route with path="*" catches any URL that doesn't match a defined route, rendering a custom 404 page.

NotFound.jsx

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="*" element={<NotFound />} />
</Routes>

37. πŸ“œ Scroll Restoration

Data routers include a <ScrollRestoration> component that automatically resets scroll position on navigation, mimicking traditional multi-page browsing behavior.

ScrollRestorationExample.jsx

import { ScrollRestoration } from "react-router-dom";

function RootLayout() {
  return (
    <>
      <Outlet />
      <ScrollRestoration />
    </>
  );
}

38. 🧳 Navigation State

Both <Link> and navigate() accept a state option, letting you pass data to the next route without putting it in the URL.

NavigationState.jsx

navigate("/checkout", { state: { fromCart: true } });

// In the destination component
const location = useLocation();
console.log(location.state.fromCart); // true

Caution

Navigation state is lost on a full page refresh, so never store data there that the page absolutely needs to function.

39. 🏷️ Route Metadata

The handle property lets you attach arbitrary metadata β€” like page titles or breadcrumb labels β€” to a route, retrievable later via useMatches.

RouteMetadata.jsx

{
  path: "/settings",
  element: <Settings />,
  handle: { crumb: () => <span>Settings</span> },
}

40. πŸ—‚οΈ Route Organization

As an app grows, keep route definitions in a dedicated file (or split by feature) rather than inline in main.jsx, so the overall navigation structure stays easy to scan.

src
main.jsx
routes
index.jsx
dashboardRoutes.jsx
authRoutes.jsx

41. πŸ—„οΈ Data Routers

"Data router" refers to routers created via createBrowserRouter (or createHashRouter, createMemoryRouter) that support loader, action, and errorElement β€” unlike the plain <BrowserRouter> from Section 5.

Router TypeSupports loaders/actions?Typical use
BrowserRouterNoSimple apps, legacy code
createBrowserRouterYesModern apps, data loading
createMemoryRouterYesTesting, non-browser environments

42. πŸ“ File-Based Routing Overview

Some meta-frameworks built on React Router β€” like Remix β€” infer routes automatically from the file system, rather than a manually written configuration array. A file named products.$productId.jsx automatically becomes the route /products/:productId.

Reference

File-based routing trades explicit configuration for convention β€” convenient for larger apps, but worth understanding the manual configuration approach first since it underpins how file-based routing works internally.

43. πŸ”· TypeScript with React Router

React Router ships with TS types out of the box. Typing useParams and useLoaderData ensures your components can't accidentally reference a parameter or field that doesn't exist.

TypedRoute.tsx

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

function ProductDetail() {
  const { productId } = useParams<{ productId: string }>();
  const product = useLoaderData() as Product;

  return <h1>{product.name}</h1>;
}

44. ⚑ Performance Optimization

  • Use lazy loading (Section 30) so route bundles are only downloaded when needed
  • Rely on loader data fetching rather than useEffect, avoiding a render-then-fetch waterfall
  • Use defer for non-critical, slow-loading data so the rest of the page can render immediately
  • Avoid unnecessary re-renders by memoizing layout components that wrap frequently-changing <Outlet> content

45. πŸ† Best Practices

  1. Prefer the RouterProvider + createBrowserRouter pattern for new projects
  2. Use loaders for data-fetching instead of component-level useEffect
  3. Group protected routes under a single layout-style guard rather than repeating checks
  4. Always provide an errorElement for routes that fetch data
  5. Keep route configuration organized in dedicated files as the app grows

46. ⚠️ Common Mistakes

  • Forgetting that <Outlet> is required for nested/child routes to render at all
  • Using <a href> instead of <Link to>, causing an unwanted full page reload
  • Placing the wildcard path="*" route before more specific routes, accidentally shadowing them
  • Storing critical data only in navigation state, which disappears on refresh
  • Mixing <BrowserRouter> usage with data-router-only hooks like useLoaderData

Danger

Hooks like useLoaderData, useActionData, and useRouteError only work inside a data router created with createBrowserRouter β€” they will throw if used under a plain <BrowserRouter>.

47. πŸ’¬ Frequently Asked Questions

Should I use BrowserRouter or createBrowserRouter for a new project?

For new projects, createBrowserRouter with <RouterProvider> is recommended β€” it unlocks loaders, actions, and better error handling that <BrowserRouter> doesn't support.

How do I redirect after a form submission?

Return a redirect(path) response from an action function, or call navigate(path) imperatively after an async operation completes inside an event handler.

Can I nest protected routes inside public ones?

Yes β€” wrap only the protected subset in a guard layout route (Section 28), while leaving sibling routes like a login or marketing page outside of it.

48. πŸ“Œ Summary

>>Good routing makes an app feel like a set of real, linkable places β€” not just a pile of screens.