React: React Router: Advanced Topics
Last updated: 2026-08-26
Tom has successfully implemented the basic routing, but new requirements keep coming up: users need to be automatically redirected to the dashboard after logging in; certain pages require a login to access; as the app grows, the home page takes longer to load; and the product list page needs to support filter parameters in the URL... He needs to learn the advanced features of React Router to handle these real-world scenarios.
1. What You'll Learn
- useNavigate: Programmatic navigation (login redirection, back and forward)
- The Route Guard component implements authentication and authorization
- React.lazy + Suspense for on-demand loading
- useSearchParams: Manage URL query parameters
- Strategies for Organizing and Extracting Routing Configurations
2. Conceptual Diagrams
flowchart TD
U[User Actions] --> A{Are you logged in??}
A -->|Not logged in| B[ProtectedRoute<br/>→ Redirect to /login]
A -->|Logged in| C{Permissions?}
C -->|No permission| D[→ 403 Page]
C -->|Has permission| E[Load the target page]
E --> F{Is the component lazily loaded??}
F -->|is | G[Suspense<br/>Show loading]
G --> H[Rendering Page]
F -->|No| H
style A fill:#fff3e0,stroke:#f57c00
style B fill:#ffcdd2,stroke:#d32f2f
style G fill:#e1f5fe,stroke:#0288d1
style H fill:#e8f5e9,stroke:#388e3c
User request → Route guard checks for login/permissions → Page loads upon approval (displays "loading" during lazy loading) → Final rendering.
3. A Real-Life Scenario
Tom's app requires the following: After a successful login, the user should be automatically redirected to the dashboard; the dashboard page must be login-protected; only users with the "admin" role should have access to the admin panel; the home page should load quickly on the first screen (with large pages loaded on demand); and product list page URLs must include filtering and pagination parameters to facilitate sharing.
(1) Programmatic navigation: useNavigate
Tom needs to handle form submissions on the login page and, upon successful submission, redirect users to different pages based on their roles. This type of navigation, triggered by code logic, cannot use <Link> (since links can only be triggered by user clicks); instead, useNavigate must be used.
useNavigate Returns a navigate function that supports three calling methods:
| Usage | Effect | Example |
|---|---|---|
navigate('/path') |
Jump to a specified path (add a new entry to the history stack) | navigate('/dashboard') |
navigate('/path', { replace: true }) |
Replace current history (cannot go back to this point) | Redirect after login |
navigate(-1) |
Back | Return to Previous Page |
▶ Example 1: Programmatic Navigation After Logging In
Output:
Multi-page navigation with React Router
import { useNavigate, useLocation } from 'react-router-dom'
function LoginPage() {
const navigate = useNavigate()
const location = useLocation()
// from URL Retrieve the URL to redirect to after login from the query parameters
const from = location.state?.from?.pathname || '/dashboard'
function handleLogin(event) {
event.preventDefault()
const formData = new FormData(event.target)
const username = formData.get('username')
// Simulate a login request
fakeLogin(username).then(user => {
if (user.role === 'admin') {
navigate('/admin', { replace: true }) // Replace Record,Cannot go back to the login page
} else {
navigate(from, { replace: true }) // Redirect to the page you were trying to access before logging in
}
})
}
// Login Form
return (
<form onSubmit={handleLogin}>
<input name="username" placeholder="Username" required />
<button type="submit">Log In</button>
<button type="button" onClick={() => navigate(-1)}>Back</button>
</form>
)
}
// Simulated Login API
async function fakeLogin(username) {
await new Promise(r => setTimeout(r, 500))
return { name: username, role: username === 'admin' ? 'admin' : 'user' }
}
Output:
navigate("/dashboard") → programmatic redirect. navigate(-1) → go back. Used after login, form submit, etc.
The role of useLocation: location.state It can receive state data passed from Link or navigate. For example, when redirecting to a login page within a ProtectedRoute, you can store the path of the page the user was originally trying to access in state, so that after logging in, the user is automatically redirected back to that page.
(2) Route Guard: ProtectedRoute
| Guard Type | Check Logic | Behavior on Failure | Typical Scenarios |
|---|---|---|---|
| Login Guard | isAuthenticated |
Redirect to /login |
Dashboard, My Account |
| Permissions Guard | user.role === 'admin' |
Redirect to /403 |
Admin Panel |
| Feature Switch Guard | featureFlags.xEnabled |
Redirect to /upgrade |
Paid Features |
| Conditional Guard | profileComplete |
Redirect to /onboarding |
First-time Login Guide |
Tom's dashboard page requires a login to access, and the admin panel requires the "admin" role. He needs a "guard" mechanism—one that checks the user's status before rendering the page and redirects them if the conditions aren't met.
At its core, RouteGuard is a wrapper component: it accepts child components and executes validation logic before rendering. If the validation passes, it renders the child components; if not, it redirects to another page using <Navigate>.
▶ Example 2: Multi-layer routing guard
Output:
Displays: "Log In". Buttons: Log In, navigate(-1)}>Back. Input: Username. Form with submit handling. React Router handles navigation. Async data fetching/loading states. Timer-based behavior
import { Navigate, useLocation } from 'react-router-dom'
// Simulated Certification Hook
function useAuth() {
return {
user: { name: 'Tom', role: 'admin' }, // In actual projects, starting from Context or store Get
isAuthenticated: true
}
}
// First Floor:Login Guard
function ProtectedRoute({ children }) {
const { isAuthenticated } = useAuth()
const location = useLocation()
if (!isAuthenticated) {
// Save the user's destination to state in ,Redirect back after logging in
return <Navigate to="/login" state={{ from: location }} replace />
}
return children
}
// Second Floor:Character Guard
function AdminRoute({ children }) {
const { user } = useAuth()
if (user.role !== 'admin') {
return <Navigate to="/403" replace />
}
return children
}
// Used in router configuration
function AppRoutes() {
return (
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/403" element={<AccessDenied />} />
{/* You must log in */}
<Route path="/dashboard" element={
<ProtectedRoute>
<Dashboard />
</ProtectedRoute>
} />
{/* You must log in + admin Character */}
<Route path="/admin" element={
<ProtectedRoute>
<AdminRoute>
<AdminPanel />
</AdminRoute>
</ProtectedRoute>
} />
</Routes>
)
}
Output:
URL routing: / → Home, /about → About, /contact → Contact. Navigation without page reload.
Nested Guard Pattern: The outer ProtectedRoute checks "whether the user is logged in," while the inner AdminRoute checks "whether the user has admin permissions." This separates responsibilities and allows for reusability. If the "Edit" role is needed in the future, simply add an EditorRoute.
(3) Lazy loading: React.lazy + Suspense
As the app grew, Tom noticed that the homepage was taking longer and longer to load—because all the code for every page was bundled into a single bundle, users had to download the admin panel code whenever they visited the homepage. React.lazy allows component code to be split into separate chunks that are loaded only when needed.
import { lazy, Suspense } from 'react'
// These components will not be bundled into the main bundle in
const Dashboard = lazy(() => import('./pages/Dashboard'))
const Settings = lazy(() => import('./pages/Settings'))
const AdminPanel = lazy(() => import('./pages/AdminPanel'))
const UserList = lazy(() => import('./pages/UserList'))
▶ Example 3: Lazy Loading at the Route Level
Output:
React Router handles navigation
import { lazy, Suspense } from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'
// Lazy-load all page components
const Home = lazy(() => import('./pages/Home'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const ProductList = lazy(() => import('./pages/ProductList'))
const ProductDetail = lazy(() => import('./pages/ProductDetail'))
const Settings = lazy(() => import('./pages/Settings'))
const NotFound = lazy(() => import('./pages/NotFound'))
// Loading component
function PageLoader() {
return (
<div style={{
display: 'flex', justifyContent: 'center', alignItems: 'center',
height: '100vh', fontSize: '1.2rem', color: '#666'
}}>
<div className="spinner" />
<span style={{ marginLeft: '12px' }}>Page is loading...</span>
</div>
)
}
function App() {
return (
<BrowserRouter>
{/* Suspense Wrap all lazy-loading routes */}
<Suspense fallback={<PageLoader />}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/products" element={<ProductList />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route path="/settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</BrowserRouter>
)
}
export default App
Output:
URL routing: / → Home, /about → About, /contact → Contact. Navigation without page reload.
How Lazy Loading Works: React only dynamically loads the /dashboard chunk when a user visits /dashboard for the first time. During loading, the fallback component from Suspense is displayed. Once loading is complete, it is replaced with the actual Dashboard component.
Performance Benefits: Assuming the original bundle is 500 KB, lazy loading reduces the main bundle to 100 KB, with each page weighing in at about 80 KB. When users visit the home page, they only need to download 100 KB instead of 500 KB—resulting in a roughly 5-fold improvement in above-the-fold loading speed.
4. useSearchParams: URL Query Parameter Management
| URL Parameter Method | API | Syntax Example | Applicable Data |
|---|---|---|---|
| Path Parameter | useParams() |
/products/:id → { id: '42' } |
Required Identifier (Resource ID) |
| Query Parameters | useSearchParams() |
?sort=price&page=2 |
Optional filtering/sorting/paging |
| Hash | useLocation().hash |
#section-3 |
Page Anchor Links |
| State | useLocation().state |
navigate('/path', { state }) |
Passing Hidden Data Across Pages |
Tom's product list page needs to support features such as category filtering, price sorting, and pagination, and these filter criteria must be reflected in the URL—so that users can share the filtered links with others.
(1) Reading and Writing Query Parameters
useSearchParams Returns a Map-like object that allows you to read and write URL query parameters in the same way as Map. Similar to useState, it returns a value and a setter.
import { useSearchParams } from 'react-router-dom'
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams()
const category = searchParams.get('category') || 'All'
const sort = searchParams.get('sort') || 'default'
const page = Number(searchParams.get('page')) || 1
function updateFilter(key, value) {
setSearchParams(prev => {
if (value) prev.set(key, value)
else prev.delete(key)
return prev
})
}
return (
<div>
<p>Current Category:{category} | Sort:{sort} | Page: {page}</p>
<button onClick={() => updateFilter('category', 'Electronics')}>Electronics Categories</button>
<button onClick={() => updateFilter('sort', 'price')}>Sort by Price</button>
<button onClick={() => updateFilter('page', String(page + 1))}>Next Page</button>
<button onClick={() => setSearchParams({})}>Clear Filters</button>
</div>
)
}
// URL It will be updated in real time:/products?category=Electronics&sort=price&page=2
▶ Example 4: Complete Product Filtering Functionality
Output:
Displays: "Page is loading...". React Router handles navigation
import { useSearchParams } from 'react-router-dom'
// Simulated Product Data
const allProducts = [
{ id: 1, name: 'iPhone 16', category: 'Electronics', price: 6999 },
{ id: 2, name: 'React Programming Books', category: 'Books', price: 79 },
{ id: 3, name: 'Mechanical Keyboard', category: 'Electronics', price: 399 },
{ id: 4, name: 'Introduction to Design Patterns', category: 'Books', price: 59 },
{ id: 5, name: 'Bluetooth Headphones', category: 'Electronics', price: 899 },
]
function ProductListPage() {
const [searchParams, setSearchParams] = useSearchParams()
// from URL Read Filter Criteria
const category = searchParams.get('category') || ''
const sortBy = searchParams.get('sort') || 'name'
const page = parseInt(searchParams.get('page') || '1', 10)
const pageSize = 3
// Filtering
let filtered = category
? allProducts.filter(p => p.category === category)
: allProducts
// Sort
if (sortBy === 'price') {
filtered = [...filtered].sort((a, b) => a.price - b.price)
} else {
filtered = [...filtered].sort((a, b) => a.name.localeCompare(b.name))
}
// Pagination
const totalPages = Math.ceil(filtered.length / pageSize)
const paged = filtered.slice((page - 1) * pageSize, page * pageSize)
// Update Filter Criteria
function setFilter(key, value) {
setSearchParams(prev => {
const next = new URLSearchParams(prev)
if (value) {
next.set(key, value)
} else {
next.delete(key)
}
next.set('page', '1') // Return to the first page when switching filters
return next
})
}
return (
<div>
<div style={{ marginBottom: '16px' }}>
<label>Categories:
<select value={category} onChange={e => setFilter('category', e.target.value)}>
<option value="">All</option>
<option value="Electronics">Electronics</option>
<option value="Books">Books</option>
</select>
</label>
<label style={{ marginLeft: '16px' }}>Sort:
<select value={sortBy} onChange={e => setFilter('sort', e.target.value)}>
<option value="name">Name</option>
<option value="price">Price</option>
</select>
</label>
</div>
<ul>
{paged.map(p => (
<li key={p.id}>{p.name} — ${p.price}({p.category})</li>
))}
</ul>
<div>
{Array.from({ length: totalPages }, (_, i) => (
<button
key={i}
onClick={() => setSearchParams(prev => {
const next = new URLSearchParams(prev)
next.set('page', String(i + 1))
return next
})}
style={{ fontWeight: page === i + 1 ? 'bold' : 'normal' }}
>
{i + 1}
</button>
))}
</div>
<p>Currently URL:/products?category={category}&sort={sortBy}&page={page}</p>
</div>
)
}
Output:
Language switcher: EN/ZH/JP. Wrapped components auto-translate. Context provides locale + t() function to all consumers.
Keyword: URL query parameters represent a "shareable state." After a user applies filters and copies the URL to send to a colleague, the colleague will see exactly the same filtered results when they open the link. This is not possible when using useState to manage filter conditions.
5. Organizational Strategies for Routing Configuration
When the number of project routes grows to several dozen, having all the routes written in a single component makes the code difficult to maintain. Tom needs to extract the route configuration into a separate module.
(1) Routing Configuration File
// src/routes/index.js
import { lazy } from 'react'
// Centrally manage all routing definitions
const routes = [
{
path: '/',
component: lazy(() => import('../pages/Home')),
exact: true
},
{
path: '/login',
component: lazy(() => import('../pages/Login')),
},
{
path: '/dashboard',
component: lazy(() => import('../pages/Dashboard')),
protected: true // You must log in
},
{
path: '/admin',
component: lazy(() => import('../pages/AdminPanel')),
protected: true,
adminOnly: true // Required admin Permissions
},
{
path: '/products',
component: lazy(() => import('../pages/ProductList')),
},
{
path: '/products/:id',
component: lazy(() => import('../pages/ProductDetail')),
},
{
path: '*',
component: lazy(() => import('../pages/NotFound')),
}
]
export default routes
▶ Example 5: Route Renderer
Output:
Uses router
// src/routes/AppRouter.jsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { Suspense } from 'react'
import routes from './index'
import ProtectedRoute from '../components/ProtectedRoute'
import AdminRoute from '../components/AdminRoute'
function renderRoutes(routeList) {
return routeList.map(route => {
const Component = route.component
let element = <Component />
// On-Demand Package Guard
if (route.protected) {
element = <ProtectedRoute>{element}</ProtectedRoute>
}
if (route.adminOnly) {
element = <AdminRoute>{element}</AdminRoute>
}
return (
<Route key={route.path} path={route.path} element={element} />
)
})
}
function AppRouter() {
return (
<BrowserRouter>
<Suspense fallback={<div>Loading......</div>}>
<Routes>
{renderRoutes(routes)}
</Routes>
</Suspense>
</BrowserRouter>
)
}
export default AppRouter
Output:
URL routing: / → Home, /about → About, /contact → Contact. Navigation without page reload.
The advantage of this approach is that routing configurations are centralized in one place, guard logic is automatically encapsulated, and adding a new page requires only adding an object to routes/index.js.
(2) useLocation: Listen for route changes
In addition to navigation, Tom also needs to monitor route changes in certain scenarios—for example, to send tracking data when the page path changes, close pop-up windows, or reset form states. useLocation can retrieve the current URL information and, when used in conjunction with useEffect, monitor path changes.
import { useLocation } from 'react-router-dom'
import { useEffect } from 'react'
function PageTracker() {
const location = useLocation()
useEffect(() => {
// Triggered whenever the path changes
console.log('Page Views:', location.pathname + location.search)
// Tracking Event Reporting
analytics.pageView({
path: location.pathname,
search: location.search,
timestamp: Date.now()
})
}, [location]) // Dependency location Object
// Note:location.pathname or location.search Any change will trigger
return null // This component does not render anything UI
}
// in App Used in
function App() {
return (
<BrowserRouter>
<PageTracker /> {/* Place Routes External,Always Listen */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/dashboard" element={<Dashboard />} />
</Routes>
</BrowserRouter>
)
}
Key properties returned by useLocation: pathname (path, such as /products/42), search (query string, such as ?category=electronics), hash (URL hash, such as #section-2), state (state data passed via Link or navigate).
❓ FAQ
useNavigate and Link?Link is used for navigation triggered by user clicks (such as links in the navigation bar, breadcrumbs, or article lists). useNavigate is used for navigation triggered by code logic (such as redirects after logging in, after form submission, scheduled redirects, or back/forward navigation). Basic rule: user interaction → Link; code logic → useNavigate.ProtectedRoute guard to check "whether the user is logged in," while the inner AdminRoute guard checks "whether the user has admin permissions." When nesting guards, ensure that each guard has a single responsibility; do not check both login status and role permissions within a single guard. If there are too many guards, consider using a configuration array combined with a loop to automatically wrap multiple layers of guards.export default); (2) It must be used inside a Suspense component; (3) It does not support server-side rendering (for SSR, use @loadable/component instead). Additionally, if the network is slow while a lazy-loaded component is loading, users will see the fallback content; it is recommended that the fallback be designed to be small and fast.useSearchParams and useState to manage filter conditions?useSearchParams synchronizes state to the URL—users can share links, bookmark pages, and navigate forward and backward. State managed by useState exists only in memory and is lost when the page is refreshed. However, useSearchParams incurs a performance overhead (URL changes trigger component re-rendering). For scenarios with frequent updates (such as dragging a slider), it’s recommended to use useState first and then synchronize the state to the URL once it’s confirmed.📖 Summary
- useNavigate handles navigation triggered by code (login redirection, back and forward navigation),
replace: trueto prevent the history stack from being corrupted - The router guard is implemented using a wrapper component; the outer layer checks for authentication, while the inner layer checks for permissions, ensuring a single responsibility and reusability.
- React.lazy + Suspense enables route-level code bundling, significantly improving first-screen load speed
- useSearchParams synchronizes filter and pagination settings to the URL, supporting link sharing and browser forward/back navigation
- Centralized management of route configurations + automatic rendering guards, ideal for maintaining large-scale projects
📝 Exercises
- Implement a login page: After the user enters their username, use
useNavigateto navigate to the dashboard, and usereplace: trueduring the navigation to prevent the user from navigating back to the login page. - Use
ProtectedRouteto protect dashboard routes, redirecting to the login page when the user is not logged in, and automatically redirecting to the page the user originally intended to visit after a successful login. - Use
useSearchParamsto implement a product list page: It should support category filtering, price sorting, and pagination. The filter criteria should be reflected in the URL, and the filter criteria should be preserved after the page is refreshed.