React / React Router
Home /React /React Router

React Router

React Router

1. What is React Router?

React Router is the standard declarative routing library for React applications. It keeps your UI in sync with the browser URL, enabling navigation between views without a full page reload, the cornerstone of Single Page Applications (SPAs).

React is a UI library. It knows how to render components, manage state, and handle events, but it has no built-in mechanism to map a URL like /about to a specific component. React Router fills that gap.

Maintained by Remix (now part of Shopify), React Router is the most widely used routing solution in the React ecosystem with hundreds of millions of downloads per month.

Key idea: React Router doesn’t navigate to new HTML pages. It intercepts URL changes and renders the appropriate React components, giving the illusion of multi-page navigation with the speed of a SPA.

2. Why Do We Need a Router?

Without a router, React renders a single page at a fixed URL. Problems arise immediately:

  • Refreshing the page always shows the same view
  • The browser Back/Forward buttons do nothing meaningful
  • You can’t share a link to a specific section like /dashboard/settings
  • Bookmarks always go back to the root

React Router solves all of this by wiring React’s component tree to the browser’s URL, history stack, and navigation APIs.

3. Installation & Setup

terminal

npm install react-router-dom

Then wrap your entire app in a router provider. The most common choice for web apps is BrowserRouter:

main.jsx

import { createRoot } from 'react-dom/client';import { BrowserRouter } from 'react-router-dom';
import App from './App';
createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

App.jsx — basic route setup

import { Routes, Route, Link } from 'react-router-dom';import Home from './pages/Home';
import About from './pages/About';
import Profile from './pages/Profile';export default function App() {
return (
    <>
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
<Link to="/profile/42">Profile</Link>      </nav>
      <Routes>
        <Route path="/"         element={<Home />} />
        <Route path="/about"     element={<About />} />
        <Route path="/profile/:id" element={<Profile />} />
      </Routes>
    </>
  );
}

4. Core Concepts

Routes & Route

Definition

<Routes> is the container that looks at the current URL and picks the first <Route> whose path matches. Only one child route renders at a time (unless you use nested routes).

Route patterns

<Routes>
  {/* Exact match */}
  <Route path="/"             element={<Home />} />

  {/* URL parameter: :id is dynamic */}
  <Route path="/users/:id"    element={<User />} />

  {/* Wildcard: matches any remaining segments */}
  <Route path="/docs/*"       element={<Docs />} />

  {/* Catch-all 404 page */}
  <Route path="*"             element={<NotFound />} />
</Routes>

Nested Routes

Nested routes let child components render inside parent layouts without re-mounting the entire tree. The parent renders <Outlet /> as the placeholder for the child.

Nested routes with Outlet

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

// DashboardLayout.jsx
import { Outlet, NavLink } from 'react-router-dom';

export default function DashboardLayout() {
  return (
    <div className="layout">
      <aside>
        <NavLink to="">Home</NavLink>
        <NavLink to="analytics">Analytics</NavLink>
        <NavLink to="settings">Settings</NavLink>
      </aside>
      <main>
        <Outlet /> {/* child route renders here */}
      </main>
    </div>
  );
}

Link vs NavLink

<Link> is the basic navigation element — renders an <a> tag that intercepts the click and calls the history API instead of reloading the page. <NavLink> extends it with an automatic active class when the URL matches the to prop — perfect for navigation menus.

NavLink with active styling

<NavLink
  to="/about"
  className={({ isActive, isPending }) =>
    isActive ? 'nav-link active' : 'nav-link'
  }
  style={({ isActive }) => ({
    color: isActive ? '#7f8ef7' : 'inherit'
  })}
>
  About
</NavLink>

Navigate & Redirect

Programmatic redirect with Navigate component

import { Navigate } from 'react-router-dom';

function ProtectedPage({ isLoggedIn }) {
  if (!isLoggedIn) {
    // Redirect to /login, replacing history entry
    return <Navigate to="/login" replace />;
  }
  return <div>Secret content</div>;
}

Search Params (Query Strings)

Reading and setting ?query=react

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

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const query = searchParams.get('q') ?? '';

  return (
    <input
      value={query}
      onChange={e => setSearchParams({ q: e.target.value })}
      placeholder="Search..."
    />
  );
  // URL becomes: /search?q=react-router
}

6. Built-in Hooks

React Router provides a set of hooks to access routing state and history from any component in the tree, without prop drilling.

Hook Returns Use case
useNavigate() navigate fn Programmatic navigation after events (form submit, auth, etc.)
useParams() { id, … } Read URL parameters like /users/:id
useLocation() location obj Access pathname, search, hash, and state
useSearchParams() [params, setter] Read and write query string parameters
useMatch() match | null Test if current URL matches a given pattern
useLoaderData() loader result Access data returned from a route loader (v6.4+)
useActionData() action result Access result from a form action (v6.4+)
useNavigation() navigation obj Track loading state during navigation (v6.4+)
useRouteError() error Access the error inside an errorElement (v6.4+)
useOutlet() outlet element Programmatically access the child route element

useNavigate — examples

useNavigate usage

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

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

  async function handleSubmit(e) {
    e.preventDefault();
    await login();

    navigate('/dashboard');          // go to /dashboard
    navigate(-1);                     // go back (like browser back)
    navigate('/login', { replace: true }); // replace history entry
    navigate('/checkout', {            // pass state
      state: { from: '/cart' }
    });
  }

  return <form onSubmit={handleSubmit}>...</form>;
}

useParams: example

Reading /products/:category/:id

import { useParams } from 'react-router-dom';
function ProductDetail() {
const { category, id } = useParams();
// URL: /products/electronics/42
// category = "electronics", id = "42"return <p>{category} — #{id}</p>;
}

useLocation: example

Accessing location object

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

function Analytics() {
  const location = useLocation();
  // {
  //   pathname: '/about',
  //   search:   '?ref=newsletter',
  //   hash:     '#team',
  //   state:    { from: '/home' },
  //   key:      'abc123'
  // }

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

7. Advanced Patterns

Protected Routes (Auth Guard)

The most common pattern, redirect unauthenticated users before a protected page mounts:

ProtectedRoute wrapper component

import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from './hooks/useAuth';

function ProtectedRoute() {
  const { user } = useAuth();
  const location = useLocation();

  if (!user) {
    // Save where user was trying to go
    return <Navigate to="/login" state={{ from: location }} replace />;
  }
  return <Outlet />;
}

// Usage in routes:
<Route element={<ProtectedRoute />}>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/settings"  element={<Settings />} />
</Route>

Lazy Loading Routes

Code-split large pages with React.lazy and Suspense — the route’s JS is only fetched when needed:

Lazy loading with Suspense

import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';

const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings  = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings"  element={<Settings />} />
      </Routes>
    </Suspense>
  );
}

Route-Level Error Boundaries (v6.4+)

Error element with useRouteError

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

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

  if (isRouteErrorResponse(error)) {
    return (
      <div>
        <h1>{error.status} {error.statusText}</h1>
        <p>{error.data}</p>
      </div>
    );
  }

  return <p>Something went wrong: {error.message}</p>;
}

Scroll Restoration

React Router v6.4+ handles scroll restoration automatically when using createBrowserRouter. For older setups, a simple hook:

useScrollToTop hook

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function ScrollToTop() {
  const { pathname } = useLocation();
  useEffect(() => window.scrollTo(0, 0), [pathname]);
  return null;
}

// Place inside BrowserRouter, before Routes
<BrowserRouter>
  <ScrollToTop />
  <App />
</BrowserRouter>

8. What Changed in v6?

React Router v6 was a major overhaul. If you’re upgrading from v5, here’s what changed:

Removed in v6

Switch → Routes

<Switch> is gone. Use <Routes>. It’s smarter, it picks the best match, not just the first one.

New in v6

element prop

Routes use element={<Component />} instead of component={Component} or render={}. Simpler and type-safe.

New in v6

Outlet

Nested layouts use <Outlet /> instead of rendering children manually. Much cleaner layout composition.

Removed in v6

useHistory → useNavigate

useHistory() is gone. The replacement useNavigate() returns a function directly — cleaner API.

New in v6.4+

Data APIs

Loaders, actions, deferred data, and RouterProvider bring Remix-style data fetching into client React apps.

Changed in v6

No exact prop

All <Route path="/"> matches are exact by default. The old exact prop is gone.

9. Frequently Asked Questions

What’s the difference between BrowserRouter and HashRouter?

BrowserRouter uses real URL paths (/about) via the HTML5 History API, which requires your server to return index.html for every route. HashRouter uses the hash fragment (/#/about), which never leaves the browser, so it works without any server configuration. Use HashRouter for static hosting (GitHub Pages, S3 without CloudFront), BrowserRouter everywhere else.

When should I use RouterProvider vs BrowserRouter?

If you need route-level data fetching (loaders), form mutations (actions), pending state during navigation, or route-level error boundaries — use createBrowserRouter + RouterProvider. For simpler apps that don’t need those features, BrowserRouter is perfectly fine and has a simpler mental model.

How do I pass state between routes?

Use the state prop on <Link> or navigate(): navigate('/checkout', { state: { items } }). Read it on the destination with useLocation().state. Note: state lives in session history — it’s gone after a hard refresh. For persistent data, use URL params or a global store.

How do I handle 404 pages?

Add a catch-all route as the last route in your <Routes>: <Route path="*" element={<NotFound />} />. The * wildcard matches anything that no other route matched. It must be last because React Router picks the best match, but a wildcard always matches.

Can I use React Router with TypeScript?

Yes, react-router-dom ships with TypeScript types built-in (no @types/ package needed). Hook return types like useParams<{ id: string }>() give you typed route params. The data router API also supports generic typing for loaders and actions.
What’s an index route?
An index route (<Route index element={...} />) is the default child rendered when the parent’s URL matches exactly and no other child route matches. For example, at /dashboard, the index route renders instead of nothing.

Is React Router compatible with React 18?

Yes. React Router v6 (and its data router additions in v6.4+) are fully compatible with React 18, including Concurrent Mode and Suspense. The data router’s loaders integrate cleanly with React.lazy and Suspense fallbacks.

How do I animate route transitions?

Use a library like Framer Motion with AnimatePresence. Wrap your <Routes> with <AnimatePresence> and use useLocation() as the key prop so Framer Motion detects route changes. Each page component wraps its content in a <motion.div> with initial, animate, and exit props.

What’s the difference between Link and a regular <a> tag?

A regular <a href="..."> triggers a full browser navigation — the server is hit and the entire React app re-initialises. <Link to="..."> intercepts the click event, updates the URL via the History API, and re-renders only the matching components. This is orders of magnitude faster and preserves all React state.

How do I test components that use React Router?

Wrap the component under test with <MemoryRouter initialEntries={['/your/path']}>. Use initialEntries to set the starting URL. If your component uses hooks like useParams, make sure the path pattern matches by also providing a <Route path="..."> inside the MemoryRouter.

Sources: