> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 09. React Router

> Master client-side routing with React Router v6+ for building multi-page applications.

# 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.

**Real-world analogy**: Think of React Router like a TV remote. In a traditional website, switching channels (pages) means turning off the TV, walking to the store to buy a new one, and turning it on again (a full page reload). With client-side routing, you press a button on the remote and the channel changes instantly -- the TV (your browser) stays on, your app state is preserved, and only the content on screen swaps out.

## 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

```bash theme={null}
npm install react-router-dom
```

***

## Basic Setup

### 1. Wrap Your App with BrowserRouter

```javascript theme={null}
// main.jsx
import { BrowserRouter } from 'react-router-dom';

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

### 2. Define Routes

```javascript theme={null}
// 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 -- use Link instead of <a> tags.
          <a> causes a full page reload; Link uses the History API
          to update the URL without reloading, keeping app state intact. */}
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/contact">Contact</Link>
      </nav>

      {/* Route Definitions -- React Router matches the current URL
          against these paths and renders the matching element.
          The path="*" wildcard catches any URL that does not match
          a defined route, acting as a 404 page. */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </div>
  );
}
```

<Warning>
  **Common pitfall -- using `<a href>` instead of `<Link to>`**: A regular `<a>` tag causes the browser to make a full server request, which destroys all your React state and re-mounts the entire app. Always use React Router's `Link` or `NavLink` for internal navigation.
</Warning>

***

## Link vs NavLink

### Link - Basic Navigation

```javascript theme={null}
import { Link } from 'react-router-dom';

<Link to="/about">About Us</Link>
<Link to="/products?category=electronics">Electronics</Link>
```

### NavLink - Active State Styling

```javascript theme={null}
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

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

### Accessing Parameters with useParams

```javascript theme={null}
import { useParams } from 'react-router-dom';

function UserProfile() {
  // useParams returns an object whose keys match the :param names
  // in the route definition. For path="/users/:userId",
  // this destructures to the actual value in the URL.
  const { userId } = useParams();
  const [user, setUser] = useState(null);

  useEffect(() => {
    // Re-fetch when userId changes (user navigates to a different profile).
    // Without [userId] in the dependency array, navigating from
    // /users/1 to /users/2 would show stale data.
    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)

```javascript theme={null}
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

```javascript theme={null}
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 -- pushes a new entry onto the history stack,
      // so the user can press the back button to return here.
      navigate('/dashboard');
      
      // replace: true -- replaces the current history entry instead of
      // pushing a new one. Use this after login so the user cannot
      // press "back" to return to the login form.
      navigate('/dashboard', { replace: true });
      
      // Relative navigation -- go back one step in browser history.
      // Equivalent to the browser's back button.
      navigate(-1);
      
      // Pass state -- ephemeral data attached to the navigation.
      // Useful for showing flash messages ("You just logged in!")
      // without polluting the URL.
      navigate('/dashboard', { state: { from: 'login' } });
    } catch (err) {
      setError('Login failed');
    }
  };

  return (/* form */);
}
```

### Accessing Navigation State

```javascript theme={null}
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:

```javascript theme={null}
// 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>
  );
}
```

```javascript theme={null}
// 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>
  );
}
```

```javascript theme={null}
// 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>
  );
}
```

<Note>
  The `end` prop on NavLink ensures it only matches exactly, not partial paths.
</Note>

***

## Protected Routes

### Basic Protected Route

**Analogy**: A protected route is like a security checkpoint at an airport. If you have a valid boarding pass (auth token), you walk through to your gate (the protected page). If not, you are redirected to the ticketing counter (the login page), and the system remembers which gate you were trying to reach so it can send you there after you get your pass.

```javascript theme={null}
import { Navigate, Outlet, useLocation } from 'react-router-dom';
import { useAuth } from './contexts/AuthContext';

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

  // Show a spinner while the auth check is in progress.
  // Without this, users see a flash of the login page on every
  // refresh before the token is read from storage.
  if (loading) {
    return <LoadingSpinner />;
  }

  if (!user) {
    // Save the attempted URL in navigation state so the login page
    // can redirect back here after successful authentication.
    // `replace` prevents the login redirect from appearing in
    // the browser history stack.
    return <Navigate to="/login" state={{ from: location }} replace />;
  }

  // <Outlet /> renders whatever child route matched.
  // This is what makes the route a "layout route" -- it wraps
  // its children with the auth check without needing to modify
  // each protected page individually.
  return <Outlet />;
}
```

### Using Protected Routes

```javascript theme={null}
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

```javascript theme={null}
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:

```javascript theme={null}
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

```javascript theme={null}
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:

```javascript theme={null}
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:

```javascript theme={null}
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

```javascript theme={null}
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>
```

***

## Common Router Pitfalls

<Warning>
  **Pitfall 1 -- Stale data after route parameter changes**: If you fetch data in a `useEffect` based on a URL parameter, make sure the parameter is in the dependency array. Otherwise, navigating from `/users/1` to `/users/2` will not re-fetch because the component stays mounted -- React Router reuses the same component instance for the same route pattern.

  ```javascript theme={null}
  function UserProfile() {
    const { userId } = useParams();

    useEffect(() => {
      // If userId is missing from the dependency array, this
      // only runs on mount. Navigating to a different user
      // shows stale data from the first user.
      fetchUser(userId);
    }, [userId]); // Always include route params here
  }
  ```

  **Pitfall 2 -- Missing `key` on route-level components**: When the same component renders for different route params (e.g., `/posts/1` and `/posts/2`), React reuses the component instance. If you need a full remount (reset form state, scroll to top, re-run all effects), add a `key` tied to the param:

  ```javascript theme={null}
  <Route
    path="/posts/:postId"
    element={<PostEditor key={postId} />}
    // Forces a full unmount/remount when postId changes
  />
  ```

  **Pitfall 3 -- Broken back button with `replace`**: Using `navigate('/path', { replace: true })` everywhere makes the back button useless. Only use `replace` when going back to the previous page would be harmful (e.g., after login, after a form submission that should not be repeated).
</Warning>

***

## 🎯 Practice Exercises

<Accordion title="Exercise 1: Blog with Dynamic Routes">
  ```javascript theme={null}
  // 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>
    );
  }
  ```
</Accordion>

<Accordion title="Exercise 2: Tab-Based Navigation with Query Params">
  ```javascript theme={null}
  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>
    );
  }
  ```
</Accordion>

***

## Summary

| Concept              | Description                     |
| -------------------- | ------------------------------- |
| **BrowserRouter**    | Wraps app for routing           |
| **Routes & Route**   | Define route paths and elements |
| **Link / NavLink**   | Navigation without page reload  |
| **useParams**        | Access URL parameters           |
| **useSearchParams**  | Access and set query parameters |
| **useNavigate**      | Programmatic navigation         |
| **useLocation**      | Current location info           |
| **Outlet**           | Render child routes             |
| **Protected Routes** | Auth-guarded routes             |
| **Lazy Loading**     | Load routes on demand           |

<Card title="Next Steps" icon="arrow-right">
  In the next chapter, you'll learn about **Optimization & Deployment** — making your React apps fast and production-ready!
</Card>

***

## Interview Deep-Dive

<AccordionGroup>
  <Accordion title="How does client-side routing work under the hood? What browser APIs does React Router use, and what happens to app state during navigation?">
    **Strong Answer:**
    Client-side routing uses the History API -- `window.history.pushState` and the `popstate` event -- to change the URL without triggering a full page reload. When you click a Link component, React Router calls `history.pushState()` to update the URL bar, then matches the new URL against your route definitions and renders the matching component. No HTTP request is sent to the server.

    The JavaScript bundle stays loaded, React's component tree stays mounted (except for components that unmount due to route changes), and all client-side state is preserved. The `popstate` event fires when the user clicks the browser's back/forward buttons. React Router listens for this and re-runs route matching.

    BrowserRouter uses clean URLs like `/dashboard/settings`. This requires server configuration: every URL must return the same `index.html`, because if a user refreshes on `/dashboard/settings`, the server receives that path and needs to serve the SPA's entry point. Without this catch-all redirect, the server returns a 404. This is the most common deployment issue with SPAs.

    HashRouter uses the URL hash (`/#/dashboard/settings`) to avoid this problem because everything after `#` is not sent to the server. But hash URLs are ugly and not SEO-friendly.

    **Follow-up: How would you implement code splitting per route, and what are the UX considerations for the loading state?**

    Use `React.lazy()` with dynamic imports for each route component. Wrap the Routes in a Suspense boundary with a fallback loading UI. When the user navigates for the first time, the chunk is fetched, the Suspense fallback shows, and once loaded, the component renders.

    For instant-feeling navigation, prefetch route chunks on link hover so the chunk loads during the hover. For nested routes, place Suspense boundaries at each layout level so that navigating between sibling routes only shows the fallback in the content area, not the entire page.
  </Accordion>

  <Accordion title="Design a protected route system that handles authentication, role-based access, and redirect-after-login. What are the edge cases?">
    **Strong Answer:**
    The architecture has three layers. First, a PrivateRoute wrapper that checks `useAuth()`. If not authenticated, it redirects to `/login` saving the attempted URL in location state. If authenticated, it renders Outlet.

    Second, a RoleRoute that checks `user.role` against `allowedRoles` and redirects to `/unauthorized` if the role does not match.

    Third, the login page reads `location.state?.from` and navigates there after successful login with `replace: true`.

    Edge cases from production: the loading state during async auth check must show a spinner, not flash the login page. Token expiry during a session means the next API call returns 401 and the fetch wrapper should clear auth state. Deep link sharing must preserve the full path including query params. The race condition on mount where the route renders before auth state resolves requires gating on `loading === false`.

    **Follow-up: How do you handle authentication in Next.js differently from a client-side SPA?**

    In a client-side SPA, auth checks happen after JavaScript loads. In Next.js with server rendering, auth checks happen on the server before HTML is sent. You read the auth cookie in a Server Component or middleware and redirect before the protected content reaches the client. This is faster and more secure. The tradeoff is requiring HTTP-only cookies instead of localStorage, which adds server-side cookie management complexity.
  </Accordion>

  <Accordion title="What is the difference between useParams, useSearchParams, and useLocation? When do you use each?">
    **Strong Answer:**
    These hooks represent three distinct parts of a URL. `useParams` reads path parameters for resource identifiers -- the thing you are viewing. `useSearchParams` reads and writes query parameters for optional view configuration like filters, sort order, and pagination. `useLocation` gives the complete location object for analytics, breadcrumbs, or navigation state.

    The design principle: path params for identity (what resource), search params for view configuration (how to display it), location state for ephemeral navigation context (where you came from).

    A common mistake is putting filter state in component state instead of search params. If a user filters products and refreshes, their filters are lost. Search params make the URL the source of truth.

    **Follow-up: How do you synchronize URL search params with React state without infinite loops?**

    Treat the URL as the single source of truth. Read from `useSearchParams` directly -- do not copy into useState. Update by calling `setSearchParams` in event handlers. This changes the URL, React Router re-renders, and the component reads new params. One render, one source of truth.

    The infinite loop trap: putting `setSearchParams` inside a useEffect that depends on search params creates a cycle. The effect sets params, triggering a re-render, which re-reads params (new reference), which re-triggers the effect. Always set params in event handlers, not effects.
  </Accordion>
</AccordionGroup>
