If you’ve built a React app at any point in the last decade, you’ve probably used React Router. It’s been the de facto standard for client‑side navigation and has evolved alongside React itself. The latest major release, React Router v7, is a non‑breaking upgrade from v6, but it unlocks a whole set of new capabilities: better server rendering, streaming, and first‑class type safety. In this article, I’m going to take you on a tour of these features and show you how to get the most out of React Router in 2025.
Why React Router at all?
Before we get into version numbers, let’s answer the foundational question: why use a router? In single‑page applications (SPAs) the browser never performs a full page reload. That means you need a way to update the URL and render different components without hitting the server. React Router solves this by intercepting navigation events, matching the current path to your route configuration, and rendering the appropriate component. It lets you build declarative links using the <Link> component, map URL paths to React elements using <Route>, and even manage nested layouts through nested routes.
In the context of modern web development, a router does more than just map URLs. React Router now offers data loading, mutation actions, error boundaries, view transitions, and prefetching. Understanding these features is crucial if you want to build fast, accessibl,e and SEO‑friendly applications.
How do I install and set up React Router?
React Router is published as the react-router package (with react-router-dom providing DOM bindings). To install it, run:
npm install react-router
Once installed, you tell React that your application will use a router. In a typical index.tsx you wrap your <App> component with <BrowserRouter> so that the router can manage navigation:
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App.tsx';
createRoot(document.getElementById('root')!).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
From here, you declare your routes with <Routes> and <Route> components. Each <Route> matches a path and renders a React element when the URL fits. Links created with <Link to="/home" /> update the URL without causing a page reload. This declarative API is the backbone of client‑side routing.
What’s new in React Router v7?
Version 7 builds on the v6 API but introduces a few important innovations:
- Non‑breaking upgrade: You can continue using your v6 code; v7 is designed to be a drop‑in replacement.
- Bridge to React 19: New bundling, server rendering, pre‑rendering and streaming features prepare your app for React 19.
- Type safety via typegen: React Router now ships with a
typegentool that generates TypeScript definitions for your routes, loader data, and actions. This means youruseLoaderData()hook can return a typed object instead ofany.
Let’s explore these changes in more detail.
Modes: Declarative vs Data vs Framework
React Router now supports three “modes” of operation that correspond to different degrees of control and integration:
- Declarative mode is the classic
<BrowserRouter>/<Routes>/<Route>API you’ve likely used for years. It’s great for SPAs that fetch data in component effects. You map paths to elements and use hooks likeuseParams()anduseSearchParams()to read the URL. - Data mode introduces the concept of a Data Router. Instead of declaring routes in JSX, you build a route object and pass it to
createBrowserRouter(). Each route can define aloader()(for pre‑render data fetching), anaction()(for mutations triggered by<Form>submissions) and an error boundary. Loaders receive arequestwith the full URL and its search parameters, allowing you to fetch data on the server or in SSR before your component mounts. This improves SEO and eliminates client‑side flicker. - Framework mode is a higher‑level integration used by frameworks like Remix. It includes server and client bundling, streaming and automatic type generation. If you’re building a full‑stack React app with file‑based routing and server rendering, you might adopt framework mode. You still use the same route objects and loader/action conventions but get build tooling and server integration out of the box.
Loaders and actions: fetch data before render
In Data and Framework modes, each route can declare a loader() function that runs before the component renders. It receives a request object with methods to inspect the URL (new URL(request.url)) and fetch data. The data returned by the loader becomes available via useLoaderData(). Here’s a simple example:
// routes.ts
import { createBrowserRouter } from 'react-router-dom';
import ProductsPage, { productsLoader } from './ProductsPage';
export const router = createBrowserRouter([
{
path: '/products',
element: <ProductsPage />, // UI component
loader: productsLoader, // pre-render fetch
action: productsAction, // optional POST/PUT handler
errorElement: <ErrorBoundary />, // per-route error handling
},
]);
// ProductsPage.tsx
export async function productsLoader({ request }) {
const url = new URL(request.url);
const q = url.searchParams.get('q') ?? '';
const sort = url.searchParams.get('sort') ?? 'relevance';
const page = Math.max(1, Number(url.searchParams.get('page') || '1'));
const data = await fetchProducts({ q, sort, page });
return { data, q, sort, page };
}
export default function ProductsPage() {
const { data, q, sort, page } = useLoaderData();
// render table or list
}
TSXLoaders run on the server in SSR or on the client during navigation. Because your data is fetched before the component renders, there’s no flash of loading state and search engines see the final markup. Actions complement loaders by handling form submissions. A <Form> component automatically posts to the action() of the nearest route. This convention streamlines mutations and state transitions.
Nested routes and layouts
React Router supports nested routes: a parent route defines a layout component and child routes render within it. In v7 the recommended way to declare nested routes in Data mode is via nested route objects. In Declarative mode you can nest <Route> components inside <Routes> but you need to render an <Outlet> in your layout component. For example:
// Declarative nested routes
import { Routes, Route, Outlet, Link } from 'react-router-dom';
const Dashboard = () => (
<div>
<h1>Dashboard</h1>
<nav>
<Link to="overview">Overview</Link>
<Link to="reports">Reports</Link>
</nav>
<Outlet />
</div>
);
const App = () => (
<Routes>
<Route path="dashboard" element={<Dashboard />}>
<Route path="overview" element={<Overview />} />
<Route path="reports" element={<Reports />} />
</Route>
</Routes>
);
TSXIn Data mode you’d declare a parent route object with children and use <Outlet> in your layout component. Nested routes allow you to compose UI and share data or error boundaries across sections.
Typegen: generate TypeScript definitions for routes
One of the biggest complaints about earlier versions of React Router was the lack of type safety: useLoaderData() returned any and params were untyped. React Router v7 fixes this with the typegen CLI. Running npx react-router typegen scans your route config and generates .d.ts files in a .react-router directory. These files declare types for params, loader data, action data and the route tree.
After running typegen you can import these types and get full autocompletion and compile‑time errors if you use unknown params or return the wrong data shape. It’s a game changer for teams working in TypeScript because it removes guesswork and documentation drift.
How does React Router handle server rendering and streaming?
React Router v7 was built alongside Remix and is designed to support server rendering and streaming out of the box. The new framework mode integrates with bundlers to pre‑render routes on the server, stream HTML to the client and hydrate on demand. In practice, this means:
- Pre‑rendering: your loaders run on the server and return data before HTML is sent. This improves SEO and reduces time to first contentful paint.
- Streaming: content is sent to the browser in chunks as soon as it’s ready instead of waiting for the entire page. React 18’s
Suspenseenables streaming of nested routes and their data. - Hydration on demand: only the interactive parts of the page are hydrated, reducing JavaScript payload and improving performance.
If you aren’t using a full‑stack framework you can still benefit from these improvements by adopting Data Routers and running your loaders in an Express or Vite SSR environment.
Frequently asked questions
Do I have to upgrade to v7 immediately?
No. React Router v7 is a non‑breaking upgrade for v6 users. You can install the new version and your existing declarative code will continue to work. However, the new typegen and data loading features are compelling reasons to plan a migration.
Can I mix declarative and data routes?
Absolutely. You can start with the declarative API and gradually adopt Data mode by creating route objects for specific sections. Both modes share the same underlying engine.
What if I’m using Remix?
Remix 2.0 uses React Router v7 under the hood. Upgrading Remix 1.0 projects to v2 involves adopting React Router v7 conventions. The official Remix upgrade guide covers the steps.
How do error boundaries work with routes?
Each route can define an errorElement That renders when its loader or action throws an error. Nested routes propagate errors up to the nearest boundary. This feature helps isolate failures and display meaningful messages without crashing the entire app.
Conclusion
React Router has matured from a simple client‑side router into a full‑featured routing platform. Version 7 preserves the familiar API while adding Data mode for pre‑render data fetching, typegen for static type safety and framework mode for server rendering and streaming. The non‑breaking upgrade means you can experiment with these features incrementally and adopt them where they make sense.
If you’re building a modern React app in 2025, understanding React Router v7 is essential. Start by installing the package and wrapping your app in <BrowserRouter>. Then, try migrating a page to a Data Router with a loader and see how much smoother your UX becomes. Finally, run react-router typegen to generate types and enjoy compile‑time confidence.
Feel free to explore my other posts on query parameters and state management to see these techniques in action. The more you experiment, the better your routing intuition will become.