React Router has modified significantly over time, however the primary thought behind it stays easy.
A URL adjustments. The router matches that URL in opposition to your route configuration. React then renders the interface related to the most effective matching route.
React Router v8 builds on that mannequin whereas cleansing up among the legacy package deal construction. Most significantly for current tutorials, react-router-dom is gone. The first APIs now come from react-router, whereas RouterProvider and HydratedRouter are offered by means of react-router/dom.
On this information, we are going to stick with Declarative Mode and construct routing with HashRouter, Routes, and Route. That retains the ideas straightforward to see whereas nonetheless utilizing the present React Router v8 API.
Alongside the way in which, we are going to add lazy-loaded pages, nested routes, dynamic parameters, shared layouts, programmatic navigation, redirects, and a correct 404 fallback.
Earlier than writing any routes, there may be one necessary distinction to know.
Older React Router tutorials generally begin with:
import {
Routes,
Route,
Hyperlink,
} from "react-router-dom";
Don’t use that for a brand new React Router v8 venture.
React Router v7 consolidated the packages whereas preserving react-router-dom round as a compatibility layer. Model 8 removes that package deal solely.
Set up React Router with:
npm set up react-router
Then import the declarative routing APIs instantly:
import {
HashRouter,
Hyperlink,
Navigate,
Outlet,
Route,
Routes,
useNavigate,
useParams,
} from "react-router";
That is the import model we are going to use all through the article.
React Router v8 additionally raises its platform baseline to Node 22.22+, React 19.2.7+, and Vite 7+ for Framework Mode. The packages at the moment are ESM-only.
For the straightforward client-side software on this article, essentially the most seen migration change is the package deal identify.
Let’s start with the smallest helpful software.
import {
HashRouter,
Hyperlink,
Route,
Routes,
} from "react-router";
import Dwelling from "./pages/Dwelling";
import About from "./pages/About";
import NotFound from "./pages/NotFound";
export default perform App() {
return (
<HashRouter>
<Navigation />
<Routes>
<Route path="/" aspect={<Dwelling />} />
<Route path="/about" aspect={<About />} />
<Route path="*" aspect={<NotFound />} />
</Routes>
</HashRouter>
);
}
perform Navigation() {
return (
<nav>
<Hyperlink to="/">Dwelling</Hyperlink>
<Hyperlink to="/about">About</Hyperlink>
</nav>
);
}
There are three necessary items right here.
HashRouter supplies the routing context and shops the appliance location within the hash portion of the URL.
Routes examines the present location and renders the route department that finest matches it. That wording issues as a result of trendy React Router performs route rating fairly than merely selecting routes in accordance with the order by which you occurred to write down them. The official documentation describes Routes as rendering the department that finest matches the present location.
Lastly, every Route describes the connection between a URL sample and a component.
For instance:
<Route path="/about" aspect={<About />} />
implies that /about ought to render the About element.
As a result of we’re utilizing HashRouter, navigation is saved after the #.
Your URLs will look roughly like this:
https://instance.com/#/
https://instance.com/#/about
https://instance.com/#/merchandise
The hash will not be despatched to the server, which makes this routing technique handy for static internet hosting environments the place server-side rewrite guidelines are unavailable.
The ultimate route makes use of a wildcard:
<Route path="*" aspect={<NotFound />} />
It catches places that don’t match one other route.
Go to one thing like:
/#/this-page-does-not-exist
and React Router renders NotFound.
This offers the appliance a client-side 404 expertise with out manually inspecting window.location.hash.
HashRouter observes the hash location, Routes finds the most effective matching route, and the matching aspect is rendered.
Navigation inside the appliance ought to usually use Hyperlink.
As an alternative of:
<a href="/about">About</a>
use:
import { Hyperlink } from "react-router";
<Hyperlink to="/about">About</Hyperlink>
There is a vital nuance right here.
It’s too simplistic to say that each <a> routinely sends an HTTP request whereas each <Hyperlink> doesn’t.
The true distinction is that Hyperlink participates in React Router’s client-side navigation system. React Router can replace the placement and render the brand new route with out performing a standard full-document navigation.
This retains the appliance mounted whereas the route adjustments.
It additionally offers the router management over navigation state and historical past.
Use common anchors for locations that ought to behave like regular doc navigation, particularly exterior web sites:
<a
href="https://instance.com"
goal="_blank"
rel="noreferrer"
>
Exterior web site
</a>
For routes owned by your React software, use Hyperlink.
A router determines which web page ought to seem.
That additionally makes route boundaries pure locations to separate your JavaScript.
Suppose the appliance accommodates a number of pages:
import Dwelling from "./pages/Dwelling";
import About from "./pages/About";
import Dashboard from "./pages/Dashboard";
import Merchandise from "./pages/Merchandise";
Static imports put these modules into the appliance’s dependency graph instantly.
For a small venture, that could be completely fantastic.
Bigger functions can profit from loading web page code solely when it turns into mandatory.
React supplies lazy() for this.
import { lazy } from "react";
const Dwelling = lazy(() => import("./pages/Dwelling"));
const About = lazy(() => import("./pages/About"));
const NotFound = lazy(() => import("./pages/NotFound"));
The dynamic import() offers your bundler a code-splitting boundary.
As an alternative of treating each web page as a part of one preliminary chunk, it might create separate chunks which can be requested when required.
A lazy element can’t render till its module is out there.
React’s Suspense supplies the non permanent interface displayed throughout that wait.
import {
lazy,
Suspense,
} from "react";
import {
HashRouter,
Hyperlink,
Route,
Routes,
} from "react-router";
const Dwelling = lazy(() => import("./pages/Dwelling"));
const About = lazy(() => import("./pages/About"));
const NotFound = lazy(() => import("./pages/NotFound"));
export default perform App() {
return (
<HashRouter>
<Navigation />
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" aspect={<Dwelling />} />
<Route path="/about" aspect={<About />} />
<Route path="*" aspect={<NotFound />} />
</Routes>
</Suspense>
</HashRouter>
);
}
perform PageLoader() {
return <p>Loading web page...</p>;
}
Now think about the consumer begins on the homepage.
The Dwelling module is required as a result of React must render it.
The consumer then clicks About.
React encounters the lazy About element and requests its module. Whereas that request is unresolved, Suspense renders PageLoader.
As soon as the module turns into obtainable, React renders About.
The About web page is requested solely when its lazy element must render, whereas Suspense supplies a brief fallback.
Watch out with claims reminiscent of “ten pages means ten instances sooner.”
Actual bundle efficiency doesn’t work that means.
Functions have shared dependencies. Recordsdata are compressed. Browsers cache sources. Community latency, parsing, compilation, and execution additionally contribute to startup efficiency.
A extra correct profit is that this:
route-level code splitting can scale back the quantity of page-specific JavaScript required for the preliminary render.
That may make a considerable distinction in a big software with out counting on unrealistic efficiency math.
That is the place routing begins turning into far more helpful than merely switching pages.
Contemplate a product part containing these URLs:
/merchandise
/merchandise/123
/merchandise/new
They’re totally different pages, however they in all probability share interface components.
Maybe each product web page has the identical heading, toolbar, filters, sidebar, or breadcrumbs.
You would repeat that construction in each element.
Nested routes give us a greater choice.
<Routes>
<Route
path="/merchandise"
aspect={<ProductsLayout />}
>
<Route
index
aspect={<ProductList />}
/>
<Route
path="new"
aspect={<NewProduct />}
/>
<Route
path=":productId"
aspect={<ProductDetail />}
/>
</Route>
</Routes>
/merchandise is now the father or mother route.
The three little one states are:
/merchandise
/merchandise/new
/merchandise/:productId
The father or mother supplies the shared format.
Our ProductsLayout element must specify the place React Router ought to place the matched little one.
That’s the job of Outlet.
import {
Hyperlink,
Outlet,
} from "react-router";
export default perform ProductsLayout() {
return (
<part className="merchandise">
<header>
<h1>Merchandise</h1>
<nav>
<Hyperlink to="/merchandise">
All Merchandise
</Hyperlink>
<Hyperlink to="/merchandise/new">
Add Product
</Hyperlink>
</nav>
</header>
<essential>
<Outlet />
</essential>
</part>
);
}
Go to:
/merchandise
and the outlet renders:
<ProductList />
Go to:
/merchandise/new
and the identical outlet renders:
<NewProduct />
Open:
/merchandise/123
and it turns into:
<ProductDetail />
The encircling ProductsLayout stays in place.
The father or mother Merchandise route owns the shared format, whereas Outlet renders the kid route chosen by the URL.
That is the actual benefit of nested routing.
It lets the URL hierarchy mirror the UI hierarchy.
There’s one fascinating line in our merchandise configuration:
<Route index aspect={<ProductList />} />
An index route is the default little one of its father or mother.
It doesn’t want its personal path.
Given:
<Route
path="/merchandise"
aspect={<ProductsLayout />}
>
<Route
index
aspect={<ProductList />}
/>
</Route>
visiting:
/merchandise
renders ProductsLayout, then locations ProductList inside its Outlet.
This sample turns into significantly helpful for dashboards.
<Route
path="/dashboard"
aspect={<DashboardLayout />}
>
<Route
index
aspect={<Overview />}
/>
<Route
path="analytics"
aspect={<Analytics />}
/>
<Route
path="settings"
aspect={<Settings />}
/>
</Route>
Now the URL construction clearly describes the interface construction.
Product pages often can’t have a manually declared route for each product.
You want a dynamic phase.
React Router represents dynamic segments with a colon:
<Route
path=":productId"
aspect={<ProductDetail />}
/>
As a result of this route is nested beneath /merchandise, it might match URLs reminiscent of:
/merchandise/42
/merchandise/123
/merchandise/keyboard
/merchandise/react-router-book
The element can learn the worth with useParams.
import { useParams } from "react-router";
export default perform ProductDetail() {
const { productId } = useParams();
return (
<article>
<h2>Product Particulars</h2>
<p>Product ID: {productId}</p>
</article>
);
}
For this URL:
/merchandise/123
the result’s successfully:
productId === "123";
Keep in mind that URL parameters are strings.
In case your software expects a numeric database ID, validate it earlier than utilizing it.
const id = Quantity(productId);
if (!Quantity.isInteger(id) || id <= 0) {
return <p>Invalid product ID.</p>;
}
In a manufacturing software, that validated worth may then be handed to a question, loader, API name, or state selector.
Hyperlinks cowl navigation initiated instantly by the consumer.
Functions additionally must navigate as a consequence of logic.
A typical instance is login.
import { useNavigate } from "react-router";
export default perform LoginForm() {
const navigate = useNavigate();
async perform handleSubmit(occasion) {
occasion.preventDefault();
const success = await login();
if (success) {
navigate("/dashboard");
}
}
return (
<type onSubmit={handleSubmit}>
<button kind="submit">
Signal In
</button>
</type>
);
}
useNavigate() returns a navigation perform.
You’ll be able to name it after a type submission, authentication occasion, deletion, checkout, onboarding step, or one other software motion.
It’s also possible to exchange the present historical past entry:
navigate("/dashboard", {
exchange: true,
});
That’s helpful when the earlier location mustn’t stay as a significant vacation spot within the browser historical past.
The wildcard route offers us our fallback:
<Route
path="*"
aspect={<NotFound />}
/>
As an alternative of routinely throwing the customer again to the homepage, a extra helpful 404 web page can provide a transparent means out.
import { Hyperlink } from "react-router";
export default perform NotFound() {
return (
<essential>
<h1>Web page Not Discovered</h1>
<p>
The web page you requested doesn't exist.
</p>
<Hyperlink to="/">
Return Dwelling
</Hyperlink>
</essential>
);
}
Computerized redirects should not at all times good UX.
A customer could need to examine the wrong URL, copy it, or just perceive what occurred.
Nonetheless, in case your software genuinely requires a delayed redirect, useNavigate can deal with it safely.
import { useEffect } from "react";
import { useNavigate } from "react-router";
export default perform NotFound() {
const navigate = useNavigate();
useEffect(() => {
const timer = window.setTimeout(() => {
navigate("/", {
exchange: true,
});
}, 3000);
return () => {
window.clearTimeout(timer);
};
}, [navigate]);
return <p>Redirecting to the homepage...</p>;
}
The cleanup perform prevents the timer from remaining lively after the element unmounts.
Generally a redirect is just a part of the route configuration.
Suppose an previous software used:
/catalog
however the brand new part lives at:
/merchandise
You’ll be able to redirect the previous route with Navigate.
import { Navigate } from "react-router";
<Route
path="/catalog"
aspect={
<Navigate
to="/merchandise"
exchange
/>
}
/>
The exchange choice replaces the present historical past entry as a substitute of pushing one other one.
This prevents the browser’s Again button from returning the consumer to a route that instantly redirects once more.
We now have sufficient items to construct the entire router.
import {
lazy,
Suspense,
} from "react";
import {
HashRouter,
Hyperlink,
Navigate,
Outlet,
Route,
Routes,
} from "react-router";
const Dwelling = lazy(() =>
import("./pages/Dwelling")
);
const About = lazy(() =>
import("./pages/About")
);
const ProductList = lazy(() =>
import("./pages/ProductList")
);
const ProductDetail = lazy(() =>
import("./pages/ProductDetail")
);
const NewProduct = lazy(() =>
import("./pages/NewProduct")
);
const NotFound = lazy(() =>
import("./pages/NotFound")
);
export default perform App() {
return (
<HashRouter>
<Navigation />
<Suspense fallback={<PageLoader />}>
<Routes>
<Route
path="/"
aspect={<Dwelling />}
/>
<Route
path="/about"
aspect={<About />}
/>
<Route
path="/merchandise"
aspect={<ProductsLayout />}
>
<Route
index
aspect={<ProductList />}
/>
<Route
path="new"
aspect={<NewProduct />}
/>
<Route
path=":productId"
aspect={<ProductDetail />}
/>
</Route>
<Route
path="/catalog"
aspect={
<Navigate
to="/merchandise"
exchange
/>
}
/>
<Route
path="*"
aspect={<NotFound />}
/>
</Routes>
</Suspense>
</HashRouter>
);
}
perform Navigation() {
return (
<nav>
<Hyperlink to="/">Dwelling</Hyperlink>
<Hyperlink to="/about">About</Hyperlink>
<Hyperlink to="/merchandise">Merchandise</Hyperlink>
</nav>
);
}
perform ProductsLayout() {
return (
<part>
<h1>Merchandise</h1>
<nav>
<Hyperlink to="/merchandise">
All Merchandise
</Hyperlink>
<Hyperlink to="/merchandise/new">
New Product
</Hyperlink>
</nav>
<Outlet />
</part>
);
}
perform PageLoader() {
return (
<p function="standing">
Loading web page...
</p>
);
}
Regardless of overlaying a number of options, the routing configuration stays readable.
You’ll be able to see the general public URLs, nested hierarchy, dynamic segments, redirect, and fallback with out tracing a set of handbook if statements.
That’s precisely what a routing layer ought to provide you with.
We’ve got intentionally used HashRouter all through this text.
It’s helpful for understanding routing and stays sensible when deploying to static internet hosting the place you can’t configure server rewrites.
The consequence seems to be like this:
https://instance.com/#/merchandise/123
For a lot of regular internet functions, nonetheless, you’ll in all probability choose BrowserRouter.
import {
BrowserRouter,
Routes,
Route,
} from "react-router";
The URLs turn out to be cleaner:
https://instance.com/merchandise/123
The tradeoff is that your server or internet hosting platform have to be configured to serve the appliance appropriately when somebody instantly requests a client-side route.
The routing ideas themselves stay almost an identical.
Study one and switching between them is simple.
React Router v8 is far bigger than the API proven on this tutorial.
The official documentation organizes React Router round Declarative, Knowledge, and Framework modes.
This text deliberately makes use of Declarative Mode:
<HashRouter>
<Routes>
<Route />
</Routes>
</HashRouter>
It’s the clearest strategy to be taught route matching, nested layouts, parameters, and navigation.
Knowledge Mode takes a distinct method by making a router configuration:
import {
createBrowserRouter,
} from "react-router";
const router = createBrowserRouter([
{
path: "/",
Component: Root,
},
]);
It allows router-level knowledge APIs and different capabilities past what <Routes> alone supplies. In actual fact, the React Router documentation explicitly notes that routes declared instantly inside <Routes> don’t take part in knowledge loading, actions, route-module code splitting, or different route-module options.
Framework Mode goes additional and may configure routes by means of app/routes.ts, route modules, the Vite integration, rendering methods, and different framework-level options.
These deserve their very own article.
For understanding the basics, Declarative Mode stays an excellent place to begin.
React Router v8 doesn’t require you to rethink routing from scratch.
The most important seen change for builders coming from older tutorials is the package deal cleanup. react-router-dom is gone, and most APIs now come instantly from react-router.
The underlying ideas stay acquainted.
HashRouter supplies hash-based navigation. Routes finds the route department that finest matches the present location. Route connects URL patterns with UI.
Hyperlink supplies router-aware navigation.
React’s lazy and Suspense can break up web page elements into chunks which can be loaded when wanted.
Nested routes permit a number of pages to share the identical format, whereas Outlet determines the place the lively little one seems.
Dynamic segments reminiscent of :productId make URLs helpful software enter, and useParams offers the element entry to these values.
Lastly, useNavigate handles navigation triggered by software logic, whereas Navigate supplies a declarative choice for redirects.
React Router v8 can go a lot additional with Knowledge and Framework modes, however these fundamentals nonetheless type a helpful psychological mannequin.
As soon as the connection between the URL, route tree, format, and rendered element turns into clear, routing stops feeling like infrastructure and begins turning into a part of the appliance’s structure.
A URL tells you the place you’re. A superb router determines what the appliance ought to turn out to be once you get there.





