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.
Navigation
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 });
}
}
Linkfor normal navigation (never a plain<a href>for internal links — it reloads the whole app).NavLinkadds an active state for menus.useNavigatefor navigation after an action. Usereplaceafter 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.