Bobby Encoded
PostsAbout
PostsAbout

© 2026 Bobby Jose

← Back to Blog

React Router: Complete Navigation Guide

March 7, 2025 · 8 min read

React, React Router, JavaScript, Navigation, Interview Prep

Introduction

Single-page applications need client-side routing to handle navigation without full page reloads. React Router is the standard solution, and understanding it deeply is essential for any React developer.

This guide covers React Router v6, which introduced significant API changes from v5.


Basic Setup

import { BrowserRouter, Routes, Route } from 'react-router-dom';

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>
  );
}

Navigation with Link

import { Link, NavLink } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      {/* Basic link */}
      <Link to="/">Home</Link>

      {/* NavLink - adds active class automatically */}
      <NavLink
        to="/about"
        className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
      >
        About
      </NavLink>

      {/* NavLink with style */}
      <NavLink
        to="/contact"
        style={({ isActive }) => ({
          fontWeight: isActive ? 'bold' : 'normal'
        })}
      >
        Contact
      </NavLink>
    </nav>
  );
}

Dynamic Routes and Parameters

// Route definition
<Route path="/users/:userId" element={<UserProfile />} />
<Route path="/posts/:category/:postId" element={<Post />} />

// Accessing parameters
import { useParams } from 'react-router-dom';

function UserProfile() {
  const { userId } = useParams();

  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  if (!user) return <Loading />;

  return <Profile user={user} />;
}

function Post() {
  const { category, postId } = useParams();
  // Both parameters available
}

Optional Parameters

// React Router v6 handles this with separate routes
<Route path="/search" element={<Search />} />
<Route path="/search/:query" element={<Search />} />

function Search() {
  const { query } = useParams();
  // query is undefined if not in URL
}

Nested Routes

Nested routes are powerful for layouts and sub-navigation.

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="dashboard" element={<Dashboard />}>
            <Route index element={<DashboardHome />} />
            <Route path="analytics" element={<Analytics />} />
            <Route path="settings" element={<Settings />} />
          </Route>
          <Route path="users" element={<Users />}>
            <Route index element={<UserList />} />
            <Route path=":userId" element={<UserProfile />} />
          </Route>
        </Route>
      </Routes>
    </BrowserRouter>
  );
}

// Layout renders children via Outlet
import { Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div className="app">
      <Header />
      <main>
        <Outlet /> {/* Child routes render here */}
      </main>
      <Footer />
    </div>
  );
}

function Dashboard() {
  return (
    <div className="dashboard">
      <DashboardSidebar />
      <div className="dashboard-content">
        <Outlet /> {/* Nested dashboard routes */}
      </div>
    </div>
  );
}

Index Routes

The index route renders when the parent path matches exactly. It's the "default" child route - like a welcome page before selecting sub-navigation.


Programmatic Navigation

import { useNavigate, useLocation } from 'react-router-dom';

function LoginForm() {
  const navigate = useNavigate();
  const location = useLocation();

  // Get the page user was trying to access
  const from = location.state?.from?.pathname || '/dashboard';

  const handleLogin = async (credentials) => {
    await login(credentials);

    // Navigate with replace (can't go back to login)
    navigate(from, { replace: true });
  };

  const handleCancel = () => {
    navigate(-1); // Go back
  };

  return (
    <form onSubmit={handleLogin}>
      {/* form fields */}
      <button type="button" onClick={handleCancel}>Cancel</button>
      <button type="submit">Login</button>
    </form>
  );
}

Passing State

// Passing state
navigate('/user/123', {
  state: { from: 'search', query: 'john' }
});

// Reading state
function UserProfile() {
  const location = useLocation();
  const { from, query } = location.state || {};

  return (
    <div>
      {from === 'search' && (
        <Link to={`/search?q=${query}`}>← Back to search</Link>
      )}
    </div>
  );
}

Query Parameters

import { useSearchParams } from 'react-router-dom';

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();

  // Read params
  const category = searchParams.get('category');
  const sort = searchParams.get('sort') || 'name';
  const page = parseInt(searchParams.get('page') || '1');

  // Update params
  const handleCategoryChange = (newCategory) => {
    setSearchParams(prev => {
      prev.set('category', newCategory);
      prev.set('page', '1'); // Reset to first page
      return prev;
    });
  };

  const handlePageChange = (newPage) => {
    setSearchParams(prev => {
      prev.set('page', String(newPage));
      return prev;
    });
  };

  return (
    <div>
      <CategoryFilter
        value={category}
        onChange={handleCategoryChange}
      />
      <SortDropdown value={sort} onChange={/* ... */} />
      <ProductGrid category={category} sort={sort} page={page} />
      <Pagination
        currentPage={page}
        onChange={handlePageChange}
      />
    </div>
  );
}

Protected Routes

Implement authentication-based route protection:

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

function ProtectedRoute({ children }) {
  const { isAuthenticated, isLoading } = useAuth();
  const location = useLocation();

  if (isLoading) {
    return <LoadingScreen />;
  }

  if (!isAuthenticated) {
    // Redirect to login, saving the attempted URL
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return children;
}

// Usage in routes
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/login" element={<Login />} />

      {/* Protected routes */}
      <Route
        path="/dashboard"
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        }
      />

      {/* Protected route with nested routes */}
      <Route
        path="/admin"
        element={
          <ProtectedRoute>
            <AdminLayout />
          </ProtectedRoute>
        }
      >
        <Route index element={<AdminHome />} />
        <Route path="users" element={<AdminUsers />} />
      </Route>
    </Routes>
  );
}

Role-Based Protection

function RequireRole({ children, allowedRoles }) {
  const { user, isAuthenticated } = useAuth();
  const location = useLocation();

  if (!isAuthenticated) {
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  if (!allowedRoles.includes(user.role)) {
    return <Navigate to="/unauthorized" replace />;
  }

  return children;
}

// Usage
<Route
  path="/admin"
  element={
    <RequireRole allowedRoles={['admin', 'superadmin']}>
      <AdminPanel />
    </RequireRole>
  }
/>

UX Tip for Protected Routes

When building Glucoplate's premium features, I learned to always save the intended destination in location state. After login or upgrade, users should land where they were trying to go - not a generic dashboard. Small UX detail, big impact on conversion.


Route Loaders (React Router v6.4+)

Data fetching integrated with routing:

import {
  createBrowserRouter,
  RouterProvider,
  useLoaderData
} from 'react-router-dom';

const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    children: [
      {
        path: 'users/:userId',
        element: <UserProfile />,
        loader: async ({ params }) => {
          const user = await fetchUser(params.userId);
          return user;
        },
        errorElement: <UserError />
      }
    ]
  }
]);

function UserProfile() {
  const user = useLoaderData(); // Data from loader

  return <Profile user={user} />;
}

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

When to Use Loaders

Route loaders are ideal when data fetching is tightly coupled to the URL. They enable parallel data fetching and better loading states. For complex apps, consider combining with React Query for caching and background updates.


Navigation Hooks Summary

HookPurpose
useNavigate()Programmatic navigation
useLocation()Current location object (pathname, search, state)
useParams()URL parameters from dynamic segments
useSearchParams()Query string parameters
useMatch(pattern)Check if current URL matches pattern
useLoaderData()Data from route loader

Interview Questions

Q1: How do you handle 404 pages in React Router?

Answer: Use a catch-all route with path="*" as the last route:

<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
  <Route path="*" element={<NotFound />} />
</Routes>

The * matches any path not matched by previous routes.

Q2: What's the difference between Link and Navigate?

Answer:

  • <Link> is a component for declarative navigation - renders an anchor tag for user clicks
  • <Navigate> is a component that redirects immediately when rendered - used for programmatic redirects in JSX
  • useNavigate() hook provides a function for programmatic navigation in event handlers

Q3: How do you persist state across navigation?

Answer: Several options:

  1. URL state - Query params (useSearchParams)
  2. Location state - navigate('/path', { state: data }) and useLocation().state
  3. Global state - Context, Redux, Zustand
  4. Session/Local storage - For persistence across refreshes

URL state is preferred for shareable/bookmarkable state; location state for temporary navigation context.

Q4: How do you prevent navigation (e.g., unsaved form)?

Answer: Use the useBlocker hook (v6.4+) or <Prompt> (v5):

import { useBlocker } from 'react-router-dom';

function FormPage() {
  const [isDirty, setIsDirty] = useState(false);

  useBlocker(
    ({ currentLocation, nextLocation }) =>
      isDirty && currentLocation.pathname !== nextLocation.pathname
  );

  return <form>{/* ... */}</form>;
}

Common Mistakes

1. Forgetting to Wrap with Router

// ❌ Error: useNavigate can only be used within Router
function App() {
  return <Navigation />; // Uses useNavigate inside
}

// ✅ CORRECT
function App() {
  return (
    <BrowserRouter>
      <Navigation />
    </BrowserRouter>
  );
}

2. Not Handling Loading States with Dynamic Routes

// ❌ Crashes if userId changes before fetch completes
function UserProfile() {
  const { userId } = useParams();
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetchUser(userId).then(setUser);
  }, [userId]);

  return <div>{user.name}</div>; // user might be null!
}

// ✅ Handle loading
function UserProfile() {
  const { userId } = useParams();
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    setLoading(true);
    fetchUser(userId)
      .then(setUser)
      .finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <Spinner />;
  if (!user) return <NotFound />;

  return <div>{user.name}</div>;
}

3. Using Index as Route Key

// ❌ Don't do this with dynamic route generation
{routes.map((route, index) => (
  <Route key={index} path={route.path} element={route.element} />
))}

// ✅ Use stable key
{routes.map(route => (
  <Route key={route.path} path={route.path} element={route.element} />
))}

Summary

  • BrowserRouter wraps your app; Routes contains route definitions
  • Nested routes use <Outlet /> for child rendering
  • Dynamic segments (:userId) accessed via useParams()
  • Programmatic navigation with useNavigate()
  • Protected routes redirect unauthenticated users
  • Query params managed with useSearchParams()

Next up: API Integration with React Query and data fetching patterns.


Part 4 of the React Developer Reference series.

← Previous

React API Integration: Data Fetching Mastery

Next →

React Component Patterns: Composition and Reusability