Updated June 2026. Tested on React 19 and React Router 7. Part of the Techalyst React series.

React on its own has no concept of pages. The moment your app needs more than one screen with real URLs, back-button support, and shareable links, you reach for React Router, which is the de facto routing library. The current data-router setup is clean: you describe your routes as data, and a few hooks handle the rest.

Defining routes

You build a router from an array of route objects and render it through a provider:

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

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

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

The :id in the path is a parameter, whatever sits in that segment of the URL is captured under that name.

Reading params

Inside the matched component, useParams gives you the URL parameters, and useSearchParams gives you the query string:

function Product() {
  const { id } = useParams();              // /products/3 -> id is "3"
  const [searchParams] = useSearchParams(); // ?tab=reviews
  const tab = searchParams.get('tab');
  return <div>Product {id}, tab {tab}</div>;
}

Navigating

For links the user clicks, use Link, which renders an anchor but navigates without a full page reload:

<Link to="/products/3">View product</Link>

For navigating in code, after a form submits or a login succeeds, use useNavigate:

const navigate = useNavigate();
navigate('/products/3');
navigate(-1); // go back

Nested routes and shared layouts

This is where the router earns its keep. Most apps have a shell, a nav bar, a sidebar, a footer, that should stay put while the inner content changes. You express that with nested routes: a parent route renders the layout, and an <Outlet /> marks where its child route should appear.

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />, // renders nav + <Outlet />
    children: [
      { index: true, element: <Home /> },
      { path: 'products/:id', element: <Product /> },
      { path: 'about', element: <About /> },
    ],
  },
]);
function Layout() {
  return (
    <>
      <NavBar />
      <main><Outlet /></main> {/* the matched child renders here */}
    </>
  );
}

Now Home, Product, and About all render inside the same layout, which you wrote once. The index: true route is the default child shown at the parent's exact path. Nest further, and you build sections with their own sub-layouts the same way.

Loading data

The data router can also load data before a route renders, through a loader on the route, so the component does not flash a spinner while it fetches. It is a good fit for straightforward page data. For client-side caching, refetching, and the rest, a dedicated library like TanStack Query is the more complete answer, and the two are often used together.

Wrapping up

React Router turns a React app into a real multi-page experience. Define routes as data with createBrowserRouter, read URL and query params with useParams and useSearchParams, and move around with Link and useNavigate. The piece worth mastering is nested routes with <Outlet />, which let you write a shared layout once and slot every page inside it. Get that structure right early and adding screens stays a one-line route entry rather than a refactor.