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



2. Conceptual Diagrams

100%
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:

TEXT 📖 Display only
Multi-page navigation with React Router
JSX
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' }
}
▶ Try it Yourself

Output:

TEXT 📖 Display only
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:

TEXT 📖 Display only
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
JSX
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:

TEXT 📖 Display only
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.

JSX
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'))
▶ Try it Yourself

▶ Example 3: Lazy Loading at the Route Level

Output:

TEXT 📖 Display only
React Router handles navigation
JSX
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
▶ Try it Yourself

Output:

TEXT 📖 Display only
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.

JSX
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
▶ Try it Yourself

▶ Example 4: Complete Product Filtering Functionality

Output:

TEXT 📖 Display only
Displays: "Page is loading...". React Router handles navigation
JSX
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:

TEXT 📖 Display only
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

JSX
// 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
▶ Try it Yourself

▶ Example 5: Route Renderer

Output:

TEXT 📖 Display only
Uses router
JSX
// 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
▶ Try it Yourself

Output:

TEXT 📖 Display only
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.

JSX
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>
  )
}
▶ Try it Yourself

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

Q How do I choose between useNavigate and Link?
A 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.
Q Can route guards be nested multiple levels deep? Are there any precautions to keep in mind?
A Yes. A common pattern is for the outer 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.
Q What are the limitations of React.lazy lazy loading?
A There are three limitations: (1) It can only be used with components exported using the default export (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.
Q What is the difference between using useSearchParams and useState to manage filter conditions?
A 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.
Q What is the difference between route guards and middleware?
A Route guards are interceptors at the React component level—they wrap around the target component and, after checking conditions, decide whether to render the component or redirect. Middleware are interceptors at the Next.js server-side level—they execute logic (such as authentication, redirection, and A/B testing) after a request reaches the server but before the page is rendered. React Router guards run on the client-side, while Next.js middleware runs on the Edge Runtime server-side.

📖 Summary


📝 Exercises

  1. Implement a login page: After the user enters their username, use useNavigate to navigate to the dashboard, and use replace: true during the navigation to prevent the user from navigating back to the login page.
  2. Use ProtectedRoute to 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.
  3. Use useSearchParams to 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.
Web-Tutorial.com

Web-Tutorial Tech Team

A team of developers maintaining programming tutorials. Each tutorial is written and reviewed by developers with expertise in that field. We work to keep our content accurate and reliable — if you spot an issue, please let us know.

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏