Articles › React

React Router Guide: Routes, Nested Layouts, Params and Protected Pages

Build multi-page React apps with React Router: route setup, nested layouts with Outlet, URL params, search params, navigation, protected routes, lazy loading and 404 pages.

Single-page applications still need URLs: users bookmark pages, share links and use the back button, and search engines index URLs. React Router maps URLs to components while keeping navigation instant. This guide covers everything you need for a real application — the same patterns this very website uses for its bilingual routes.

Setup

npm install react-router-dom
import { BrowserRouter } from "react-router-dom";

createRoot(document.getElementById("root")).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

BrowserRouter uses the History API for clean URLs (/articles/react-router-guide). Your server must return index.html for unknown paths so deep links work after a refresh — in ASP.NET Core that's app.MapFallbackToFile("index.html").

Defining routes

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

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/articles" element={<ArticleList />} />
      <Route path="/articles/:slug" element={<ArticlePage />} />
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

React Router ranks routes by specificity, so the order of declarations doesn't matter — /articles/new beats /articles/:slug automatically.

Nested routes and layouts

Most apps share a layout (header, footer, sidebar). Nested routes render children inside the parent's <Outlet />:

<Routes>
  <Route element={<SiteLayout />}>
    <Route index element={<Home />} />
    <Route path="articles" element={<ArticleList />} />
    <Route path="articles/:slug" element={<ArticlePage />} />
  </Route>

  <Route path="admin" element={<AdminLayout />}>
    <Route index element={<Dashboard />} />
    <Route path="users" element={<Users />} />
  </Route>
</Routes>
import { Outlet } from "react-router-dom";

function SiteLayout() {
  return (
    <>
      <Header />
      <main>
        <Outlet />   {/* child route renders here */}
      </main>
      <Footer />
    </>
  );
}

The layout stays mounted while children change — headers don't re-render or flicker, and state like an open menu survives navigation.

A language prefix as a layout

This site uses a dynamic segment for the language:

<Route path="/:lang" element={<SiteLayout />}>
  <Route index element={<HomePage />} />
  <Route path="articles/:slug" element={<ArticlePage />} />
</Route>

The layout reads lang with useParams(), validates it, sets dir="rtl" or ltr, and provides translations to every page below it.

URL parameters

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

function ArticlePage() {
  const { slug } = useParams();
  const { data, isLoading } = useArticle(slug);
  // ...
}

Params are always strings. Convert and validate them (Number(id)), and handle not-found results gracefully.

Search params for filters and pagination

Filters belong in the URL — that makes them shareable and survives refresh:

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

function ArticleList() {
  const [params, setParams] = useSearchParams();
  const tag = params.get("tag") ?? "";
  const page = Number(params.get("page") ?? 1);

  const selectTag = (next) =>
    setParams((prev) => {
      prev.set("tag", next);
      prev.set("page", "1");
      return prev;
    });

  // /articles?tag=React&page=2
}

Use { replace: true } for updates (like typing in a search box) that shouldn't create a history entry for every keystroke.

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

<Link to="/articles">All articles</Link>

<NavLink to="/articles" className={({ isActive }) => (isActive ? "active" : "")}>
  Articles
</NavLink>

function LoginForm() {
  const navigate = useNavigate();
  async function onSubmit() {
    await login();
    navigate("/dashboard", { replace: true });
  }
}
  • Link for normal navigation (never a plain <a href> for internal links — it reloads the whole app).
  • NavLink adds an active state for menus.
  • useNavigate for navigation after an action. Use replace after login so "back" doesn't return to the login page.

Protected routes

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

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

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

<Route element={<RequireAuth />}>
  <Route path="/dashboard" element={<Dashboard />} />
  <Route path="/settings" element={<Settings />} />
</Route>

After login, send the user back where they came from:

const from = location.state?.from?.pathname ?? "/dashboard";
navigate(from, { replace: true });

Warning: Route guards are a UX feature, not security. Your API must still authorize every request.

Lazy loading routes

Split heavy pages into separate bundles so the first load stays fast:

import { lazy, Suspense } from "react";

const AdminPanel = lazy(() => import("./admin/AdminPanel"));
const ArticlePage = lazy(() => import("./pages/ArticlePage"));

<Route
  path="articles/:slug"
  element={
    <Suspense fallback={<Spinner />}>
      <ArticlePage />
    </Suspense>
  }
/>

On this site, the Markdown and syntax-highlighting libraries load only when you open an article — the home page bundle is about 40% smaller because of it.

Scroll restoration and 404s

When navigating, browsers don't automatically scroll to the top in SPAs. Scroll on pathname change (or use <ScrollRestoration /> with data routers):

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

A catch-all path="*" route renders a friendly 404 page. If you also render HTML on the server, return a real 404 status for SEO.

Data routers (optional)

React Router also offers createBrowserRouter with loaders and actions, which fetch data before rendering a route and handle form submissions. In React Router v7 this evolves into a full framework mode (the successor of Remix) with server rendering. For classic SPAs, the component-based <Routes> API above is perfectly fine.

Summary

React Router turns URLs into UI: define routes, share layouts with nested routes and <Outlet />, read useParams and useSearchParams, navigate with Link and useNavigate, guard private pages, and lazy-load heavy routes. Keep filters in the URL, configure your server's fallback, and your SPA will behave like a well-structured website.

FAQ

BrowserRouter or HashRouter?

Use BrowserRouter for clean, SEO-friendly URLs. HashRouter (/#/page) is only for hosts where you can't configure a fallback.

Why does my page 404 after refresh in production?

The server is looking for a real file at that path. Configure it to serve index.html for client-side routes.

How do I pass data between routes?

Prefer the URL (params and search params). For temporary data, use navigation state; for shared data, use context or a data-fetching cache.