Routing is the backbone of every React application. Without it, your app is a single page with no way to navigate. At Boundev, React Router is a non-negotiable part of our frontend stack. This guide covers everything you need to know—from basic setup to advanced patterns like data loaders and actions.
Why React Router?
React itself has no built-in routing. Libraries like React Router fill that gap by mapping URL paths to components, enabling the seamless, instant navigation users expect from modern web applications. As of 2026, React Router v7 is the gold standard, offering a "data router" architecture that couples routing with data fetching and mutations.
Core Concepts at a Glance
Map a URL path to a component
/dashboard → <Dashboard />
Fetch data before rendering
No more loading spinners on mount
Handle mutations declaratively
Form submissions without boilerplate
1. Setting Up React Router
The modern approach uses createBrowserRouter and RouterProvider. This "data router" setup unlocks loaders, actions, and error boundaries.
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Root from './routes/Root';
import Dashboard from './routes/Dashboard';
import ErrorPage from './routes/ErrorPage';
const router = createBrowserRouter([
{
path: '/',
element: <Root />,
errorElement: <ErrorPage />,
children: [
{ path: 'dashboard', element: <Dashboard /> },
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
2. Nested Routes and Layouts
Nested routes are perhaps React Router's most powerful feature. They allow you to define a layout once (e.g., a sidebar and header) and have child routes render inside it via the <Outlet /> component.
// Root.jsx — The shared layout
import { Outlet, Link } from 'react-router-dom';
export default function Root() {
return (
<div className="app-layout">
<nav>
<Link to="/dashboard">Dashboard</Link>
<Link to="/settings">Settings</Link>
</nav>
<main>
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}
Pro Tip
Think of nested routes as a "Russian doll" pattern. Each level adds UI around its children. The header stays mounted while child pages swap in and out—giving the feel of a native app.
3. Data Loading with Loaders
Before React Router's data APIs, developers had to fetch data inside useEffect, which caused the dreaded pattern of: render → show spinner → fetch → re-render. Loaders fix this by fetching data before the component mounts.
// Route definition with a loader
{
path: 'dashboard',
element: <Dashboard />,
loader: async () => {
const res = await fetch('/api/stats');
return res.json();
},
}
// Dashboard.jsx — Consuming loader data
import { useLoaderData } from 'react-router-dom';
export default function Dashboard() {
const stats = useLoaderData();
return <h1>Revenue: ${stats.revenue}</h1>;
}
4. Mutations with Actions
Actions handle data writes (POST, PUT, DELETE). When a user submits a <Form>, React Router calls the associated action, then automatically revalidates all loaders to keep the UI fresh.
import { Form, redirect } from 'react-router-dom';
// Route definition
{
path: 'contacts/new',
element: <NewContact />,
action: async ({ request }) => {
const formData = await request.formData();
await createContact(Object.fromEntries(formData));
return redirect('/contacts');
},
}
// NewContact.jsx
export default function NewContact() {
return (
<Form method="post">
<input name="name" placeholder="Name" />
<button type="submit">Save</button>
</Form>
);
}
5. Dynamic Routes and URL Parameters
Dynamic segments (e.g., /users/:userId) let you build detail pages. The useParams hook extracts the value from the URL.
// Route
{ path: 'users/:userId', element: <UserProfile /> }
// UserProfile.jsx
import { useParams } from 'react-router-dom';
export default function UserProfile() {
const { userId } = useParams();
// Fetch user data using userId...
}
6. Protected Routes
For authenticated sections, use a loader to check the user's session. If they're not logged in, redirect them to the login page. This approach is more secure than client-side guards because it runs before any component renders.
{
path: 'admin',
element: <AdminPanel />,
loader: async () => {
const user = await getUser();
if (!user) throw redirect('/login');
return user;
},
}
7. Error Handling
React Router lets you define an errorElement per route. If a loader throws or a component crashes, the error element renders instead of a blank screen.
{
path: 'dashboard',
element: <Dashboard />,
errorElement: <ErrorPage />, // Displays if loader or component throws
loader: dashboardLoader,
}
Essential Hooks Cheat Sheet
| Hook | Purpose | Example |
|---|---|---|
| useNavigate | Programmatic navigation | navigate('/home') |
| useParams | Read URL params | const { id } = useParams() |
| useLoaderData | Access loader results | const data = useLoaderData() |
| useLocation | Read current URL info | const loc = useLocation() |
| useSubmit | Trigger actions imperatively | submit(formData, { method: 'post' }) |
Build Scalable React Applications
From routing architecture to production deployment, Boundev's engineering teams build React applications that scale effortlessly.
Talk to Our React ExpertsFrequently Asked Questions
Do I need React Router for every React project?
Not necessarily. If your app is a single-screen widget or a simple form, you don't need routing. However, any application with more than one "page" or view benefits greatly from React Router.
What is the difference between BrowserRouter and createBrowserRouter?
BrowserRouter is the older, component-based approach. createBrowserRouter is the newer "data router" API that unlocks loaders, actions, and errorElement. For new projects, always use createBrowserRouter.
How do I handle 404 pages with React Router?
Add a catch-all route at the end of your route configuration with path: '*'. This matches any URL that doesn't correspond to a defined route, and you can render a custom "Page Not Found" component.
Can I use React Router with Next.js?
No. Next.js has its own file-based routing system. React Router is designed for client-side SPAs or Vite-based projects. Mixing the two would create conflicts.
How do loaders improve performance over useEffect?
With useEffect, the component renders first (often showing a spinner), then fetches data, then re-renders. Loaders fetch data in parallel with the route transition, so the component renders with data already available—eliminating layout shift and perceived lag.
