Back to CourseIntermediate4 min read
React /

React Router: Setup and Basics

Add client side routing to a React app, define routes, link between pages and handle URL parameters.

Bishal Biswas

Bishal Biswas

Self Learner

Published on: May 14, 2026
Last updated: September 5, 2026
Intermediate Topic

Why you need a router

React renders components. It has no concept of pages or URLs.

Without a router, your whole app lives at one address. The back button does not work, users cannot bookmark anything, and refreshing loses where they were.

A router maps URLs to components and keeps the browser’s address bar in sync.

Installing

npm install react-router-dom

React Router is the most widely used option. It can run in three modes: as a library (covered here), in framework mode with data loading built in, or in declarative mode. The library setup below is the right place to start.

Defining routes

import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Home from './pages/Home.jsx';
import About from './pages/About.jsx';
import Products from './pages/Products.jsx';
import NotFound from './pages/NotFound.jsx';
const router = createBrowserRouter([
  { path: '/', element: <Home /> },
  { path: '/about', element: <About /> },
  { path: '/products', element: <Products /> },
  { path: '*', element: <NotFound /> }
]);
function App() {
  return <RouterProvider router={router} />;
}

An array of objects, each mapping a path to an element. * matches anything not matched above, which gives you a 404 page.

Linking

Never use a plain <a> for internal links.

<a href="/about">About</a>
<Link to="/about">About</Link>

An <a> triggers a full page reload: the whole app downloads and restarts, and all your state is lost. <Link> changes the URL and swaps the component, which is instant.

import { Link } from 'react-router-dom';
function Nav() {
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/about">About</Link>
      <Link to="/products">Products</Link>
    </nav>
  );
}

Use a real <a> only for external links.

import { NavLink } from 'react-router-dom';
function Nav() {
  return (
    <nav>
      <NavLink to="/" end className={({ isActive }) => (isActive ? 'active' : '')}>
        Home
      </NavLink>
      <NavLink to="/products" className={({ isActive }) => (isActive ? 'active' : '')}>
        Products
      </NavLink>
    </nav>
  );
}

NavLink passes isActive to className or style. It also sets aria-current="page" automatically, which matters for screen readers.

Note end on the home link. Without it, / counts as active on every page, because every path starts with /.

URL parameters

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

The colon marks a dynamic segment. Read it with useParams:

import { useParams } from 'react-router-dom';
function ProductDetail() {
  const { id } = useParams();
  return <h1>Product {id}</h1>;
}

Parameters are always strings, even when they look like numbers. Convert before using them as numbers:

const { id } = useParams();
const productId = Number(id);
if (Number.isNaN(productId)) return <NotFound />;

Several parameters work as you would expect:

{ path: '/users/:userId/posts/:postId', element: <Post /> }
const { userId, postId } = useParams();

Query strings

Parameters are part of the path. Query strings are for filters, sorting and pagination.

import { useSearchParams } from 'react-router-dom';
function Products() {
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get('category') ?? 'all';
  const page = Number(searchParams.get('page') ?? 1);
  function selectCategory(next) {
    setSearchParams({ category: next, page: '1' });
  }
  return (
    <div>
      <button onClick={() => selectCategory('books')}>Books</button>
      <p>Showing {category}, page {page}</p>
    </div>
  );
}

useSearchParams works like useState but the value lives in the URL. That means filters survive a refresh and can be shared as a link, which is exactly what users expect.

Note that setSearchParams replaces everything. To keep the other values:

setSearchParams((previous) => {
  previous.set('page', '2');
  return previous;
});
import { useNavigate } from 'react-router-dom';
function LoginForm() {
  const navigate = useNavigate();
  async function handleSubmit(event) {
    event.preventDefault();
    await login();
    navigate('/dashboard');
  }
  return <form onSubmit={handleSubmit}>...</form>;
}
navigate('/products');
navigate('/products', { replace: true });
navigate(-1);
navigate('/checkout', { state: { from: '/cart' } });

replace: true swaps the current entry instead of adding one, so the back button does not return to a login page the user has already passed.

Use <Link> for anything a user clicks. Use navigate only after an action, such as a successful submit.

Deployment gotcha

Client side routes do not exist as files. Visiting /about directly asks the server for a file that is not there, and you get a 404.

The fix is a host rule serving index.html for every path.

/*  /index.html  200

That is Netlify’s _redirects. Vercel does it automatically, and other hosts have an equivalent. If your links work but refreshing 404s, this is why.

Quick recap

  • A router maps URLs to components and keeps history working.
  • Use <Link> for internal navigation, never a plain <a>.
  • NavLink gives you an active state and sets aria-current.
  • Add end to the home NavLink or it is always active.
  • :id defines a parameter, read with useParams, always a string.
  • useSearchParams puts filters in the URL so they can be shared.
  • useNavigate is for navigating after an action, with replace where appropriate.
  • Configure your host to serve index.html for all paths.
script.js

import {
  createBrowserRouter, RouterProvider, Link, NavLink,
  useParams, useSearchParams, useNavigate
} from 'react-router-dom';
// Routes map a URL to a component. '*' catches everything unmatched.
const router = createBrowserRouter([
  { path: '/', element: <Home /> },
  { path: '/products', element: <Products /> },
  { path: '/products/:id', element: <ProductDetail /> },
  { path: '*', element: <NotFound /> }
]);
export default function App() {
  return <RouterProvider router={router} />;
}
function Nav() {
  return (
    <nav>
      {/* `end` is required here, or / is active on every page */}
      <NavLink to="/" end className={({ isActive }) => (isActive ? 'active' : '')}>
        Home
      </NavLink>
      {/* Link, never a plain <a>. An <a> reloads the whole app. */}
      <Link to="/products">Products</Link>
      <a href="https://example.com">External site</a>
    </nav>
  );
}
// URL parameters are ALWAYS strings, even when they look like numbers
function ProductDetail() {
  const { id } = useParams();
  const productId = Number(id);
  if (Number.isNaN(productId)) return <NotFound />;
  return <h1>Product {productId}</h1>;
}
// Query strings belong in the URL, so filters survive a refresh and can
// be shared as a link
function Products() {
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get('category') ?? 'all';
  const page = Number(searchParams.get('page') ?? 1);
  function nextPage() {
    // The updater form keeps the other parameters. Passing an object
    // would replace every one of them.
    setSearchParams((previous) => {
      previous.set('page', String(page + 1));
      return previous;
    });
  }
  return (
    <div>
      <p>Category {category}, page {page}</p>
      <button onClick={nextPage}>Next page</button>
    </div>
  );
}
// navigate() is for AFTER an action, not for things users click
function LoginForm() {
  const navigate = useNavigate();
  async function handleSubmit(event) {
    event.preventDefault();
    await login();
    // replace: true so the back button does not return to the login page
    navigate('/dashboard', { replace: true });
  }
  return <form onSubmit={handleSubmit}><button>Log in</button></form>;
}
// DEPLOYMENT: client routes are not real files, so the host must serve
// index.html for every path. Netlify _redirects:   /*  /index.html  200

Was this explanation helpful?

Your feedback helps us improve the content for everyone.