Overview
React Router enables client-side routing in React applications, allowing you to build single-page applications with multiple views and seamless navigation without full page reloads.
This section covers React Router v6 features including nested routes, loaders, actions, and programmatic navigation.
Basic Router Setup
Set up React Router using createBrowserRouter and RouterProvider:
import { createBrowserRouter , RouterProvider } from 'react-router-dom' ;
import HomePage from './pages/Home' ;
const router = createBrowserRouter ([
{ path: '/' , element: < HomePage /> },
]);
function App () {
return < RouterProvider router = { router } /> ;
}
export default App ;
The createBrowserRouter API is the recommended approach for React Router v6.4+ as it enables data loading and server-side rendering capabilities.
Nested Routes and Layouts
Create hierarchical route structures with shared layouts:
const router = createBrowserRouter ([
{
path: '/' ,
element: < RootLayout /> ,
children: [
{ index: true , element: < HomePage /> },
{
path: 'events' ,
element: < EventsRootLayout /> ,
children: [
{ index: true , element: < EventsPage /> },
{ path: ':eventId' , element: < EventDetailPage /> },
{ path: 'new' , element: < NewEventPage /> },
{ path: ':eventId/edit' , element: < EditEventPage /> },
],
},
],
},
]);
Key Concepts
Index routes - Default child route rendered at parent path
Dynamic segments - Use :parameter syntax for dynamic values
Nested layouts - Share UI elements across multiple routes
Navigation with NavLink
Use NavLink for navigation with active state styling:
import { NavLink } from 'react-router-dom' ;
function MainNavigation () {
return (
< nav >
< ul >
< li >
< NavLink
to = "/"
className = { ({ isActive }) =>
isActive ? 'active' : undefined
}
end
>
Home
</ NavLink >
</ li >
< li >
< NavLink
to = "/products"
className = { ({ isActive }) =>
isActive ? 'active' : undefined
}
>
Products
</ NavLink >
</ li >
</ ul >
</ nav >
);
}
The end prop ensures the NavLink is only active when the path matches exactly, useful for the home route.
Data Loading with Loaders
Load data before rendering route components:
Route Configuration
Component Usage
const router = createBrowserRouter ([
{
path: '/' ,
element: < RootLayout /> ,
children: [
{
path: 'events' ,
element: < EventsPage /> ,
loader : async () => {
const response = await fetch ( 'http://localhost:8080/events' );
if ( ! response . ok ) {
throw new Error ( 'Failed to fetch events' );
}
const resData = await response . json ();
return resData . events ;
},
},
],
},
]);
Loaders execute before the route component renders, blocking navigation until data is fetched. Use defer for non-blocking data loading.
Handle form submissions using route actions:
Action Implementation
Route Configuration
import { redirect } from 'react-router-dom' ;
export async function action ({ request , params }) {
const data = await request . formData ();
const eventData = {
title: data . get ( 'title' ),
image: data . get ( 'image' ),
date: data . get ( 'date' ),
description: data . get ( 'description' ),
};
const response = await fetch ( 'http://localhost:8080/events' , {
method: 'POST' ,
headers: {
'Content-Type' : 'application/json' ,
},
body: JSON . stringify ( eventData ),
});
if ( ! response . ok ) {
throw new Response ( JSON . stringify ({ message: 'Could not save event.' }), {
status: 500 ,
});
}
return redirect ( '/events' );
}
Handling Loading States
Provide user feedback during navigation:
import { useNavigation } from 'react-router-dom' ;
function AuthForm () {
const navigation = useNavigation ();
const isSubmitting = navigation . state === 'submitting' ;
return (
< form method = "post" >
{ /* form fields */ }
< button disabled = { isSubmitting } >
{ isSubmitting ? 'Submitting...' : 'Save' }
</ button >
</ form >
);
}
useNavigation provides the current navigation state: idle, loading, or submitting.
Dynamic Routes and Parameters
Access URL parameters in your components:
import { useParams } from 'react-router-dom' ;
function EventDetailPage () {
const params = useParams ();
const eventId = params . eventId ;
// Fetch and display event details
return < div > Event ID: { eventId } </ div > ;
}
Query Parameters
Work with URL query parameters:
import { useSearchParams } from 'react-router-dom' ;
function AuthForm () {
const [ searchParams ] = useSearchParams ();
const isLogin = searchParams . get ( 'mode' ) === 'login' ;
return (
< form >
< h1 > { isLogin ? 'Log in' : 'Create a new user' } </ h1 >
{ /* form fields */ }
</ form >
);
}
Best Practices
Separate loader functions
Extract loader functions to separate files for better organization and reusability: // loaders/events.js
export async function eventsLoader () {
const response = await fetch ( 'http://localhost:8080/events' );
if ( ! response . ok ) {
throw json ({ message: 'Failed to fetch events' }, { status: 500 });
}
return response ;
}
Use error boundaries and throw responses for proper error handling: const router = createBrowserRouter ([
{
path: '/' ,
element: < RootLayout /> ,
errorElement: < ErrorPage /> ,
children: [ /* routes */ ],
},
]);
Use useNavigate for navigation in response to user actions: import { useNavigate } from 'react-router-dom' ;
function MyComponent () {
const navigate = useNavigate ();
function handleSubmit () {
// Process data
navigate ( '/success' );
}
return < button onClick = { handleSubmit } > Submit </ button > ;
}
Common Patterns
Protected Routes Implement route protection with loaders: export function checkAuthLoader () {
const token = getAuthToken ();
if ( ! token ) {
return redirect ( '/auth' );
}
return null ;
}
Layout Wrappers Create reusable layout components: function RootLayout () {
return (
<>
< MainNavigation />
< Outlet />
</>
);
}
Authentication Learn how to implement authentication with React Router
React Query Alternative data fetching approach with caching