/dashboard/users/123 Home Dashboard About Contact <Outlet /> → User Profile Component REACT ROUTER v6 • NAVIGATION • SPA ROUTING
Frontend

React Router: Complete Navigation Guide

Mayur Dabhi
Mayur Dabhi
April 1, 2026
22 min read

React Router is the standard routing library for React applications, enabling you to build single-page applications with dynamic, client-side navigation. Whether you're building a simple portfolio site or a complex enterprise dashboard, understanding React Router is essential for creating seamless user experiences without full page reloads.

In this comprehensive guide, we'll explore React Router v6 from the ground up, covering everything from basic setup to advanced patterns like nested routes, protected routes, lazy loading, and programmatic navigation. By the end, you'll have the skills to implement sophisticated navigation patterns in any React application.

What You'll Learn
  • React Router v6 fundamentals and key differences from v5
  • Setting up routes with BrowserRouter and createBrowserRouter
  • Nested routes and the Outlet component
  • Dynamic routes with URL parameters
  • Programmatic navigation with useNavigate
  • Protected routes and authentication patterns
  • Code splitting with lazy loading
  • Data loading with loaders and actions
  • Navigation state and location handling
  • Best practices and common patterns

Understanding Client-Side Routing

Before diving into React Router, let's understand what client-side routing is and why it matters. In traditional multi-page applications (MPAs), every navigation triggers a full page reload from the server. In contrast, single-page applications (SPAs) use client-side routing to update the URL and render different components without reloading the page.

Traditional vs Client-Side Routing Traditional (MPA) Browser Request Server renders entire new page ❌ Full page reload Client-Side (SPA) URL Change Detected React Router matches route & renders component ✓ Instant, no reload

Client-side routing eliminates full page reloads for faster, smoother navigation

Installation and Setup

Let's start by installing React Router and setting up a basic routing configuration. React Router v6 introduced significant changes from v5, so we'll focus on the modern approach.

Terminal
# Install React Router
npm install react-router-dom

# Or with yarn
yarn add react-router-dom

# Or with pnpm
pnpm add react-router-dom

Basic Router Setup

There are two main ways to set up React Router v6: the traditional component-based approach with BrowserRouter, and the newer data API approach with createBrowserRouter. Let's explore both:

src/App.jsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

export default App;
src/App.jsx
import { 
  createBrowserRouter, 
  RouterProvider 
} from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';

const router = createBrowserRouter([
  {
    path: "/",
    element: <Home />,
  },
  {
    path: "/about",
    element: <About />,
  },
  {
    path: "/contact",
    element: <Contact />,
  },
  {
    path: "*",
    element: <NotFound />,
  },
]);

function App() {
  return <RouterProvider router={router} />;
}

export default App;
Pro Tip

The Data API approach (createBrowserRouter) is recommended for new projects as it enables powerful features like data loaders, actions, and error boundaries at the route level.

Navigation Components

React Router provides several components for navigation. Understanding when to use each one is crucial for building intuitive UIs.

Link

Basic navigation component that renders an anchor tag. Use for standard navigation links.

NavLink

Like Link but adds active styling. Perfect for navigation menus where you need to highlight the current page.

Navigate

Declarative component for redirects. Use in render logic to redirect users based on conditions.

useNavigate

Hook for programmatic navigation. Use in event handlers or after async operations.

Link and NavLink Examples

components/Navigation.jsx
import { Link, NavLink } from 'react-router-dom';

function Navigation() {
  return (
    <nav className="main-nav">
      {/* Basic Link - no active styling */}
      <Link to="/">Home</Link>
      
      {/* NavLink with active class */}
      <NavLink 
        to="/dashboard"
        className={({ isActive }) => 
          isActive ? 'nav-link active' : 'nav-link'
        }
      >
        Dashboard
      </NavLink>
      
      {/* NavLink with inline active styles */}
      <NavLink 
        to="/profile"
        style={({ isActive }) => ({
          fontWeight: isActive ? 'bold' : 'normal',
          color: isActive ? '#61dafb' : 'inherit'
        })}
      >
        Profile
      </NavLink>
      
      {/* Link with state */}
      <Link 
        to="/products" 
        state={{ from: 'navigation' }}
      >
        Products
      </Link>
      
      {/* Link with replace (no history entry) */}
      <Link to="/login" replace>
        Login
      </Link>
    </nav>
  );
}

Nested Routes and Layouts

One of React Router's most powerful features is nested routing, which allows you to create consistent layouts where only parts of the page change during navigation. This is perfect for dashboards, admin panels, and any app with a persistent sidebar or header.

Nested Routes with Outlet Root Layout Component <Header /> - Shared across all routes <Sidebar /> NavLinks • Dashboard • Users • Settings <Outlet /> Child route renders here /dashboard DashboardPage /users UsersPage /settings SettingsPage

The Outlet component renders child routes within parent layouts

src/layouts/DashboardLayout.jsx
import { Outlet, NavLink } from 'react-router-dom';

function DashboardLayout() {
  return (
    <div className="dashboard-layout">
      <header className="dashboard-header">
        <h1>Admin Dashboard</h1>
        <UserMenu />
      </header>
      
      <div className="dashboard-body">
        <aside className="sidebar">
          <nav>
            <NavLink to="/dashboard" end>
              <i className="icon-home" /> Overview
            </NavLink>
            <NavLink to="/dashboard/users">
              <i className="icon-users" /> Users
            </NavLink>
            <NavLink to="/dashboard/products">
              <i className="icon-box" /> Products
            </NavLink>
            <NavLink to="/dashboard/settings">
              <i className="icon-cog" /> Settings
            </NavLink>
          </nav>
        </aside>
        
        <main className="dashboard-content">
          {/* Child routes render here */}
          <Outlet />
        </main>
      </div>
    </div>
  );
}
src/App.jsx - Nested Route Configuration
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import DashboardLayout from './layouts/DashboardLayout';
import Overview from './pages/dashboard/Overview';
import Users from './pages/dashboard/Users';
import Products from './pages/dashboard/Products';
import Settings from './pages/dashboard/Settings';

const router = createBrowserRouter([
  {
    path: "/dashboard",
    element: <DashboardLayout />,
    children: [
      {
        index: true, // Matches /dashboard exactly
        element: <Overview />,
      },
      {
        path: "users", // Matches /dashboard/users
        element: <Users />,
      },
      {
        path: "products", // Matches /dashboard/products
        element: <Products />,
      },
      {
        path: "settings", // Matches /dashboard/settings
        element: <Settings />,
      },
    ],
  },
]);

function App() {
  return <RouterProvider router={router} />;
}
Understanding Index Routes

The index: true property creates an "index route" that renders when the parent path is matched exactly. It's equivalent to using path="" but more explicit. In the example above, the Overview component renders at /dashboard while other child routes render at their respective paths.

Dynamic Routes and URL Parameters

Dynamic routes allow you to capture variable segments from the URL. This is essential for user profiles, product pages, blog posts, and any content that needs unique URLs.

Route Configuration
const router = createBrowserRouter([
  {
    path: "/users",
    element: <UsersLayout />,
    children: [
      {
        index: true,
        element: <UsersList />,
      },
      {
        path: ":userId", // Dynamic segment
        element: <UserProfile />,
      },
      {
        path: ":userId/edit",
        element: <EditUser />,
      },
    ],
  },
  {
    // Multiple dynamic segments
    path: "/products/:category/:productId",
    element: <ProductDetail />,
  },
  {
    // Optional segments with ?
    path: "/search/:query?",
    element: <SearchResults />,
  },
  {
    // Catch-all with *
    path: "/docs/*",
    element: <Documentation />,
  },
]);

Accessing URL Parameters with useParams

pages/UserProfile.jsx
import { useParams, Link } from 'react-router-dom';
import { useState, useEffect } from 'react';

function UserProfile() {
  const { userId } = useParams();
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  
  useEffect(() => {
    async function fetchUser() {
      try {
        const response = await fetch(`/api/users/${userId}`);
        const data = await response.json();
        setUser(data);
      } catch (error) {
        console.error('Failed to fetch user:', error);
      } finally {
        setLoading(false);
      }
    }
    
    fetchUser();
  }, [userId]); // Re-fetch when userId changes
  
  if (loading) return <LoadingSpinner />;
  if (!user) return <NotFound message="User not found" />;
  
  return (
    <div className="user-profile">
      <img src={user.avatar} alt={user.name} />
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      <Link to={`/users/${userId}/edit`}>Edit Profile</Link>
    </div>
  );
}

Essential Router Hooks

React Router v6 provides several hooks for accessing routing information and performing navigation. Here are the most important ones:

Hook Purpose Returns
useNavigate Programmatic navigation Navigate function
useParams Access URL parameters Object with params
useSearchParams Read/write query strings [searchParams, setSearchParams]
useLocation Current location object { pathname, search, hash, state }
useMatch Check if route matches Match object or null
useOutletContext Access parent context Context value from Outlet

useNavigate - Programmatic Navigation

Using useNavigate
import { useNavigate } from 'react-router-dom';

function LoginForm() {
  const navigate = useNavigate();
  
  async function handleSubmit(event) {
    event.preventDefault();
    const formData = new FormData(event.target);
    
    try {
      await loginUser(formData);
      
      // Navigate to dashboard after successful login
      navigate('/dashboard');
      
      // Or with options:
      navigate('/dashboard', { 
        replace: true, // Replace current history entry
        state: { from: 'login' } // Pass state to destination
      });
      
    } catch (error) {
      setError(error.message);
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      {/* form fields */}
      <button type="submit">Login</button>
      
      {/* Go back */}
      <button type="button" onClick={() => navigate(-1)}>
        Go Back
      </button>
      
      {/* Go forward */}
      <button type="button" onClick={() => navigate(1)}>
        Go Forward
      </button>
    </form>
  );
}

useSearchParams - Query String Management

Search and Filter with Query Params
import { useSearchParams } from 'react-router-dom';

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();
  
  // Read query parameters
  const category = searchParams.get('category') || 'all';
  const sort = searchParams.get('sort') || 'newest';
  const page = parseInt(searchParams.get('page') || '1');
  
  // Update query parameters
  function handleCategoryChange(newCategory) {
    setSearchParams(prev => {
      prev.set('category', newCategory);
      prev.set('page', '1'); // Reset to page 1
      return prev;
    });
  }
  
  function handleSortChange(newSort) {
    setSearchParams(prev => {
      prev.set('sort', newSort);
      return prev;
    });
  }
  
  function handlePageChange(newPage) {
    setSearchParams(prev => {
      prev.set('page', newPage.toString());
      return prev;
    });
  }
  
  // URL becomes: /products?category=electronics&sort=price&page=2
  
  return (
    <div>
      <Filters 
        category={category}
        sort={sort}
        onCategoryChange={handleCategoryChange}
        onSortChange={handleSortChange}
      />
      <ProductGrid category={category} sort={sort} page={page} />
      <Pagination page={page} onPageChange={handlePageChange} />
    </div>
  );
}

Protected Routes and Authentication

Most applications need to protect certain routes from unauthorized access. Here's how to implement protected routes with React Router:

Protected Route Flow User Request /dashboard Authenticated? Check Auth Yes ✓ Render Route <Dashboard /> No ↩ Redirect /login?from=/dashboard

Protected routes check authentication before rendering or redirecting

components/ProtectedRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

function ProtectedRoute({ children, requiredRole }) {
  const { user, isLoading } = useAuth();
  const location = useLocation();
  
  // Show loading while checking auth
  if (isLoading) {
    return <LoadingSpinner />;
  }
  
  // Not logged in - redirect to login
  if (!user) {
    return (
      <Navigate 
        to="/login" 
        state={{ from: location.pathname }}
        replace 
      />
    );
  }
  
  // Check role if required
  if (requiredRole && user.role !== requiredRole) {
    return <Navigate to="/unauthorized" replace />;
  }
  
  // Authorized - render children
  return children;
}

// Usage in routes:
const router = createBrowserRouter([
  {
    path: "/dashboard",
    element: (
      <ProtectedRoute>
        <DashboardLayout />
      </ProtectedRoute>
    ),
    children: [/* ... */]
  },
  {
    path: "/admin",
    element: (
      <ProtectedRoute requiredRole="admin">
        <AdminPanel />
      </ProtectedRoute>
    ),
  },
]);

Redirect After Login

pages/Login.jsx
import { useNavigate, useLocation } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';

function Login() {
  const navigate = useNavigate();
  const location = useLocation();
  const { login } = useAuth();
  
  // Get the page they were trying to visit
  const from = location.state?.from || '/dashboard';
  
  async function handleSubmit(event) {
    event.preventDefault();
    const formData = new FormData(event.target);
    
    try {
      await login(formData.get('email'), formData.get('password'));
      
      // Redirect to the page they originally wanted
      navigate(from, { replace: true });
    } catch (error) {
      setError('Invalid credentials');
    }
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <h1>Login</h1>
      {from !== '/dashboard' && (
        <p className="info">
          Please log in to access {from}
        </p>
      )}
      {/* form fields */}
    </form>
  );
}

Code Splitting with Lazy Loading

Large applications benefit from code splitting, where route components are loaded only when needed. This significantly improves initial load time.

Lazy Loading Routes
import { lazy, Suspense } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';

// Lazy load route components
const Home = lazy(() => import('./pages/Home'));
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Settings = lazy(() => import('./pages/Settings'));
const Analytics = lazy(() => import('./pages/Analytics'));

// Loading component
function PageLoader() {
  return (
    <div className="page-loader">
      <div className="spinner" />
      <p>Loading...</p>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: "/",
    element: (
      <Suspense fallback={<PageLoader />}>
        <Home />
      </Suspense>
    ),
  },
  {
    path: "/dashboard",
    element: (
      <Suspense fallback={<PageLoader />}>
        <Dashboard />
      </Suspense>
    ),
  },
  // Or wrap multiple routes with a single Suspense
]);

// Better approach: Create a lazy wrapper
function LazyRoute({ component: Component }) {
  return (
    <Suspense fallback={<PageLoader />}>
      <Component />
    </Suspense>
  );
}
Named Exports with Lazy

If your component uses named exports, you can use: const Page = lazy(() => import('./Page').then(module => ({ default: module.Page })))

Data Loading with Loaders

React Router v6.4+ introduced data APIs that allow you to load data before rendering routes. This eliminates loading states in components and provides a better user experience.

Route with Loader
import { 
  createBrowserRouter, 
  useLoaderData,
  defer,
  Await 
} from 'react-router-dom';
import { Suspense } from 'react';

// Loader function runs before component renders
async function userLoader({ params }) {
  const response = await fetch(`/api/users/${params.userId}`);
  if (!response.ok) {
    throw new Response('User not found', { status: 404 });
  }
  return response.json();
}

// Deferred loader for non-critical data
function dashboardLoader() {
  return defer({
    user: fetchUser(), // Critical - wait for this
    stats: fetchStats(), // Non-critical - stream later
    notifications: fetchNotifications(), // Non-critical
  });
}

const router = createBrowserRouter([
  {
    path: "/users/:userId",
    element: <UserProfile />,
    loader: userLoader,
    errorElement: <ErrorPage />,
  },
  {
    path: "/dashboard",
    element: <Dashboard />,
    loader: dashboardLoader,
  },
]);

// Component using loader data
function UserProfile() {
  const user = useLoaderData();
  
  // No loading state needed - data is already available!
  return (
    <div className="profile">
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// Component with deferred data
function Dashboard() {
  const { user, stats, notifications } = useLoaderData();
  
  return (
    <div>
      {/* User loads immediately */}
      <WelcomeMessage user={user} />
      
      {/* Stats stream in when ready */}
      <Suspense fallback={<StatsSkeletons />}>
        <Await resolve={stats}>
          {(stats) => <StatsCards stats={stats} />}
        </Await>
      </Suspense>
      
      {/* Notifications stream in when ready */}
      <Suspense fallback={<NotificationsSkeleton />}>
        <Await resolve={notifications}>
          {(notifications) => <NotificationList data={notifications} />}
        </Await>
      </Suspense>
    </div>
  );
}

Form Handling with Actions

Actions handle form submissions and mutations. Combined with loaders, they provide a complete data flow solution.

Route with Action
import { 
  Form, 
  useActionData, 
  useNavigation,
  redirect 
} from 'react-router-dom';

// Action handles form submission
async function createUserAction({ request }) {
  const formData = await request.formData();
  
  const errors = validateUser(formData);
  if (Object.keys(errors).length) {
    return { errors };
  }
  
  try {
    const user = await createUser({
      name: formData.get('name'),
      email: formData.get('email'),
      role: formData.get('role'),
    });
    
    // Redirect after successful creation
    return redirect(`/users/${user.id}`);
  } catch (error) {
    return { errors: { form: error.message } };
  }
}

const router = createBrowserRouter([
  {
    path: "/users/new",
    element: <CreateUserForm />,
    action: createUserAction,
  },
]);

function CreateUserForm() {
  const actionData = useActionData();
  const navigation = useNavigation();
  
  const isSubmitting = navigation.state === 'submitting';
  
  return (
    <Form method="post" className="user-form">
      <h1>Create New User</h1>
      
      {actionData?.errors?.form && (
        <div className="error">{actionData.errors.form}</div>
      )}
      
      <div className="field">
        <label htmlFor="name">Name</label>
        <input 
          type="text" 
          name="name" 
          id="name"
          required 
        />
        {actionData?.errors?.name && (
          <span className="field-error">
            {actionData.errors.name}
          </span>
        )}
      </div>
      
      <div className="field">
        <label htmlFor="email">Email</label>
        <input 
          type="email" 
          name="email" 
          id="email"
          required 
        />
      </div>
      
      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? 'Creating...' : 'Create User'}
      </button>
    </Form>
  );
}

Error Handling

React Router provides built-in error handling with error boundaries at the route level.

Error Handling
import { 
  useRouteError, 
  isRouteErrorResponse,
  Link 
} from 'react-router-dom';

function ErrorPage() {
  const error = useRouteError();
  
  // Handle different error types
  if (isRouteErrorResponse(error)) {
    if (error.status === 404) {
      return (
        <div className="error-page">
          <h1>404 - Page Not Found</h1>
          <p>The page you're looking for doesn't exist.</p>
          <Link to="/">Go Home</Link>
        </div>
      );
    }
    
    if (error.status === 401) {
      return (
        <div className="error-page">
          <h1>Unauthorized</h1>
          <p>You need to log in to access this page.</p>
          <Link to="/login">Log In</Link>
        </div>
      );
    }
    
    return (
      <div className="error-page">
        <h1>{error.status} Error</h1>
        <p>{error.statusText}</p>
      </div>
    );
  }
  
  // Handle unexpected errors
  return (
    <div className="error-page">
      <h1>Oops! Something went wrong</h1>
      <p>{error.message || 'An unexpected error occurred'}</p>
      <button onClick={() => window.location.reload()}>
        Try Again
      </button>
    </div>
  );
}

// Route configuration with error boundary
const router = createBrowserRouter([
  {
    path: "/",
    element: <RootLayout />,
    errorElement: <ErrorPage />, // Catches errors in this route and children
    children: [
      {
        path: "users/:userId",
        element: <UserProfile />,
        loader: userLoader,
        // Can have its own error element
        errorElement: <UserNotFound />,
      },
    ],
  },
]);

Best Practices

Organize Routes by Feature

Group routes by feature or domain rather than by route type:

src/
├── features/
│   ├── auth/
│   │   ├── routes.jsx      # Auth routes config
│   │   ├── Login.jsx
│   │   └── Register.jsx
│   ├── dashboard/
│   │   ├── routes.jsx      # Dashboard routes
│   │   ├── Overview.jsx
│   │   └── Settings.jsx
│   └── users/
│       ├── routes.jsx      # User routes
│       ├── UserList.jsx
│       └── UserProfile.jsx
└── App.jsx                  # Combines all routes

Use Relative Links in Nested Routes

When inside nested routes, use relative paths for better maintainability:

// Inside /dashboard route
<Link to="settings">Settings</Link>    // Goes to /dashboard/settings
<Link to="../profile">Profile</Link>  // Goes to /profile
<Link to=".">Current</Link>           // Stays on current route

Quick Reference Checklist

  • ✅ Use createBrowserRouter for new projects (enables data APIs)
  • ✅ Use NavLink for navigation menus with active states
  • ✅ Implement errorElement for each route level
  • ✅ Use loader for data fetching, action for mutations
  • ✅ Lazy load routes with React.lazy for code splitting
  • ✅ Use useSearchParams for filters and pagination
  • ✅ Wrap protected routes in an authentication component
  • ✅ Use the end prop on NavLink for exact matching
  • ✅ Pass state via Link for context between pages
  • ✅ Use index routes for default child content

Conclusion

React Router is a powerful and flexible library that has evolved significantly with version 6. The new data APIs (loaders, actions, defer) bring React Router closer to full-stack frameworks like Remix, enabling more sophisticated data patterns without additional libraries.

Key takeaways from this guide:

Master these concepts, and you'll be able to build complex, performant React applications with excellent navigation experiences. The patterns shown here scale from simple portfolio sites to enterprise-level dashboards.

React Router Navigation SPA Frontend
Mayur Dabhi

Mayur Dabhi

Full Stack Developer with 5+ years of experience building scalable web applications with Laravel, React, and modern technologies.