Skip to main content

React Router

React Router is the standard routing library for React. It enables navigation between views, URL parameter handling, and keeps your UI in sync with the URL—all without page reloads.

Why Client-Side Routing?

Traditional Server Routing:
┌──────────────────────────────────────────────────────────┐
│  Click link → Request to server → Full page reload      │
│                                                          │
│  Slow, loses client state, full re-render               │
└──────────────────────────────────────────────────────────┘

Client-Side Routing (SPA):
┌──────────────────────────────────────────────────────────┐
│  Click link → JavaScript updates URL → Re-render view   │
│                                                          │
│  Fast, keeps state, smooth transitions                  │
└──────────────────────────────────────────────────────────┘

Installation

npm install react-router-dom

Basic Setup

1. Wrap Your App with BrowserRouter

// main.jsx
import { BrowserRouter } from 'react-router-dom';

ReactDOM.createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

2. Define Routes

// App.jsx
import { Routes, Route, Link } 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 (
    <div>
      {/* Navigation */}
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/contact">Contact</Link>
      </nav>

      {/* Route Definitions */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </div>
  );
}

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

<Link to="/about">About Us</Link>
<Link to="/products?category=electronics">Electronics</Link>
import { NavLink } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <NavLink 
        to="/"
        className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
      >
        Home
      </NavLink>
      
      <NavLink 
        to="/about"
        style={({ isActive }) => ({
          fontWeight: isActive ? 'bold' : 'normal',
          color: isActive ? 'blue' : 'gray'
        })}
      >
        About
      </NavLink>
    </nav>
  );
}

Dynamic Routes (URL Parameters)

Defining Dynamic Routes

<Routes>
  <Route path="/users/:userId" element={<UserProfile />} />
  <Route path="/products/:category/:productId" element={<ProductDetail />} />
</Routes>

Accessing Parameters with useParams

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

function UserProfile() {
  const { userId } = useParams();
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(setUser);
  }, [userId]);

  if (!user) return <Loading />;

  return (
    <div>
      <h1>{user.name}</h1>
      <p>User ID: {userId}</p>
    </div>
  );
}

Query Parameters (Search Params)

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

function ProductList() {
  const [searchParams, setSearchParams] = useSearchParams();
  
  const category = searchParams.get('category') || 'all';
  const sort = searchParams.get('sort') || 'name';
  const page = parseInt(searchParams.get('page')) || 1;

  const updateFilters = (newCategory) => {
    setSearchParams({ 
      category: newCategory, 
      sort,
      page: 1 // Reset to page 1 on filter change
    });
  };

  return (
    <div>
      <p>Current URL: /products?category={category}&sort={sort}&page={page}</p>
      
      <div className="filters">
        <button onClick={() => updateFilters('electronics')}>Electronics</button>
        <button onClick={() => updateFilters('clothing')}>Clothing</button>
      </div>

      <select 
        value={sort} 
        onChange={(e) => setSearchParams({ category, sort: e.target.value, page })}
      >
        <option value="name">Name</option>
        <option value="price">Price</option>
      </select>
    </div>
  );
}

Programmatic Navigation

useNavigate Hook

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

function LoginForm() {
  const navigate = useNavigate();
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    try {
      await login(credentials);
      
      // Navigate to dashboard
      navigate('/dashboard');
      
      // Or with replace (no back button)
      navigate('/dashboard', { replace: true });
      
      // Or go back
      navigate(-1);
      
      // Pass state
      navigate('/dashboard', { state: { from: 'login' } });
    } catch (err) {
      setError('Login failed');
    }
  };

  return (/* form */);
}

Accessing Navigation State

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

function Dashboard() {
  const location = useLocation();
  const from = location.state?.from;

  return (
    <div>
      {from === 'login' && <p>Welcome! You just logged in.</p>}
    </div>
  );
}

Nested Routes

Create layouts with shared navigation:
// App.jsx
function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="about" element={<About />} />
        <Route path="dashboard" element={<DashboardLayout />}>
          <Route index element={<DashboardHome />} />
          <Route path="settings" element={<Settings />} />
          <Route path="profile" element={<Profile />} />
        </Route>
      </Route>
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}
// Layout.jsx
import { Outlet } from 'react-router-dom';

function Layout() {
  return (
    <div>
      <Header />
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/dashboard">Dashboard</Link>
      </nav>
      
      {/* Renders child routes */}
      <main>
        <Outlet />
      </main>
      
      <Footer />
    </div>
  );
}
// DashboardLayout.jsx
function DashboardLayout() {
  return (
    <div className="dashboard">
      <aside>
        <NavLink to="/dashboard" end>Overview</NavLink>
        <NavLink to="/dashboard/settings">Settings</NavLink>
        <NavLink to="/dashboard/profile">Profile</NavLink>
      </aside>
      
      <section>
        <Outlet />
      </section>
    </div>
  );
}
The end prop on NavLink ensures it only matches exactly, not partial paths.

Protected Routes

Basic Protected Route

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

function ProtectedRoute() {
  const { user, loading } = useAuth();
  const location = useLocation();

  if (loading) {
    return <LoadingSpinner />;
  }

  if (!user) {
    // Save the attempted URL for redirecting after login
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  return <Outlet />;
}

Using Protected Routes

function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="login" element={<Login />} />
        
        {/* Protected Routes */}
        <Route element={<ProtectedRoute />}>
          <Route path="dashboard" element={<Dashboard />} />
          <Route path="profile" element={<Profile />} />
          <Route path="settings" element={<Settings />} />
        </Route>
      </Route>
    </Routes>
  );
}

Role-Based Protection

function RoleProtectedRoute({ allowedRoles }) {
  const { user } = useAuth();

  if (!user) {
    return <Navigate to="/login" replace />;
  }

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

  return <Outlet />;
}

// Usage
<Route element={<RoleProtectedRoute allowedRoles={['admin']} />}>
  <Route path="admin" element={<AdminDashboard />} />
</Route>

Lazy Loading Routes

Load routes only when needed:
import { lazy, Suspense } from 'react';

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

function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        
        <Route 
          path="dashboard" 
          element={
            <Suspense fallback={<PageLoader />}>
              <Dashboard />
            </Suspense>
          } 
        />
        
        <Route 
          path="settings" 
          element={
            <Suspense fallback={<PageLoader />}>
              <Settings />
            </Suspense>
          } 
        />
      </Route>
    </Routes>
  );
}

Shared Suspense Boundary

function App() {
  return (
    <Suspense fallback={<PageLoader />}>
      <Routes>
        <Route path="/" element={<Layout />}>
          <Route index element={<Home />} />
          <Route path="dashboard" element={<Dashboard />} />
          <Route path="settings" element={<Settings />} />
        </Route>
      </Routes>
    </Suspense>
  );
}

useLocation Hook

Access current location information:
import { useLocation } from 'react-router-dom';

function Breadcrumbs() {
  const location = useLocation();
  
  // location = {
  //   pathname: '/dashboard/settings',
  //   search: '?tab=security',
  //   hash: '#password',
  //   state: { from: 'profile' },
  //   key: 'abc123'
  // }
  
  const pathSegments = location.pathname.split('/').filter(Boolean);
  
  return (
    <nav className="breadcrumbs">
      <Link to="/">Home</Link>
      {pathSegments.map((segment, index) => {
        const path = '/' + pathSegments.slice(0, index + 1).join('/');
        return (
          <span key={path}>
            {' / '}
            <Link to={path}>{segment}</Link>
          </span>
        );
      })}
    </nav>
  );
}

Scroll Restoration

Reset scroll position on navigation:
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function ScrollToTop() {
  const { pathname } = useLocation();

  useEffect(() => {
    window.scrollTo(0, 0);
  }, [pathname]);

  return null;
}

// In App.jsx
function App() {
  return (
    <BrowserRouter>
      <ScrollToTop />
      <Routes>...</Routes>
    </BrowserRouter>
  );
}

Error Boundaries with Routes

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

function ErrorPage() {
  const error = useRouteError();

  if (isRouteErrorResponse(error)) {
    if (error.status === 404) {
      return <NotFoundPage />;
    }
    if (error.status === 401) {
      return <UnauthorizedPage />;
    }
  }

  return (
    <div className="error-page">
      <h1>Oops! Something went wrong</h1>
      <p>{error.message || 'Unknown error occurred'}</p>
    </div>
  );
}

// In route configuration
<Route 
  path="/" 
  element={<Layout />}
  errorElement={<ErrorPage />}
>
  {/* routes */}
</Route>

🎯 Practice Exercises

// App.jsx
function App() {
  return (
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="blog">
          <Route index element={<BlogList />} />
          <Route path=":slug" element={<BlogPost />} />
          <Route path="category/:category" element={<BlogCategory />} />
        </Route>
      </Route>
    </Routes>
  );
}

// BlogList.jsx
function BlogList() {
  const posts = [
    { slug: 'react-basics', title: 'React Basics', category: 'react' },
    { slug: 'hooks-intro', title: 'Intro to Hooks', category: 'react' },
    { slug: 'css-grid', title: 'CSS Grid Layout', category: 'css' }
  ];

  return (
    <div>
      <h1>Blog</h1>
      {posts.map(post => (
        <article key={post.slug}>
          <Link to={`/blog/${post.slug}`}>
            <h2>{post.title}</h2>
          </Link>
          <Link to={`/blog/category/${post.category}`}>
            {post.category}
          </Link>
        </article>
      ))}
    </div>
  );
}

// BlogPost.jsx
function BlogPost() {
  const { slug } = useParams();
  const navigate = useNavigate();
  
  // Fetch post by slug...
  
  return (
    <article>
      <button onClick={() => navigate(-1)}>← Back</button>
      <h1>Post: {slug}</h1>
    </article>
  );
}
function SettingsPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  const currentTab = searchParams.get('tab') || 'general';

  const tabs = [
    { id: 'general', label: 'General', component: GeneralSettings },
    { id: 'security', label: 'Security', component: SecuritySettings },
    { id: 'notifications', label: 'Notifications', component: NotificationSettings }
  ];

  const ActiveComponent = tabs.find(t => t.id === currentTab)?.component || GeneralSettings;

  return (
    <div className="settings-page">
      <h1>Settings</h1>
      
      <div className="tabs">
        {tabs.map(tab => (
          <button
            key={tab.id}
            className={currentTab === tab.id ? 'active' : ''}
            onClick={() => setSearchParams({ tab: tab.id })}
          >
            {tab.label}
          </button>
        ))}
      </div>

      <div className="tab-content">
        <ActiveComponent />
      </div>
    </div>
  );
}

Summary

ConceptDescription
BrowserRouterWraps app for routing
Routes & RouteDefine route paths and elements
Link / NavLinkNavigation without page reload
useParamsAccess URL parameters
useSearchParamsAccess and set query parameters
useNavigateProgrammatic navigation
useLocationCurrent location info
OutletRender child routes
Protected RoutesAuth-guarded routes
Lazy LoadingLoad routes on demand

Next Steps

In the next chapter, you’ll learn about Optimization & Deployment — making your React apps fast and production-ready!