πŸ—ΊοΈ React Router

Building Multi-Page Applications

What is React Router?

React Router enables navigation between different views/pages in your React app. It keeps your UI in sync with the URL and provides a smooth single-page application (SPA) experience.

Why React Router?

  • Navigate between pages without full reload
  • URL parameters and query strings
  • Nested routes for complex layouts
  • Programmatic navigation
  • Protected routes for authentication

πŸ“¦ Installation

npm install react-router-dom

# Version 6+ (recommended)
npm install react-router-dom@6

🎯 Basic Setup

// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

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

// App.jsx
import { Routes, Route, Link } from 'react-router-dom';

function App() {
  return (
    <div>
      {/* Navigation */}
      <nav>
        <Link to="/">Home</Link>
        <Link to="/about">About</Link>
        <Link to="/contact">Contact</Link>
      </nav>
      
      {/* Routes */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
      </Routes>
    </div>
  );
}

function Home() {
  return <h1>Home Page</h1>;
}

function About() {
  return <h1>About Page</h1>;
}

function Contact() {
  return <h1>Contact Page</h1>;
}

πŸ”— Link vs NavLink

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

function Navigation() {
  return (
    <nav>
      {/* Regular Link */}
      <Link to="/">Home</Link>
      
      {/* NavLink - adds 'active' class to current route */}
      <NavLink 
        to="/about"
        className={({ isActive }) => isActive ? 'active' : ''}
      >
        About
      </NavLink>
      
      {/* With styling */}
      <NavLink
        to="/products"
        style={({ isActive }) => ({
          color: isActive ? 'red' : 'black',
          fontWeight: isActive ? 'bold' : 'normal'
        })}
      >
        Products
      </NavLink>
    </nav>
  );
}

🎯 URL Parameters

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

function App() {
  return (
    <Routes>
      {/* Dynamic route with parameter */}
      <Route path="/users/:userId" element={<UserProfile />} />
      <Route path="/posts/:postId" element={<Post />} />
      <Route path="/products/:category/:id" element={<Product />} />
    </Routes>
  );
}

// Access params with useParams
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 <div>Loading...</div>;
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

// Multiple parameters
function Product() {
  const { category, id } = useParams();
  
  return (
    <div>
      <p>Category: {category}</p>
      <p>Product ID: {id}</p>
    </div>
  );
}

// Usage
<Link to="/users/123">User 123</Link>
<Link to="/products/electronics/456">Product</Link>

πŸ” Query Parameters

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

function SearchPage() {
  const [searchParams, setSearchParams] = useSearchParams();
  
  // Get query parameters
  const query = searchParams.get('q') || '';
  const page = searchParams.get('page') || '1';
  const sort = searchParams.get('sort') || 'name';
  
  const handleSearch = (newQuery) => {
    setSearchParams({ q: newQuery, page: '1', sort });
  };
  
  const handlePageChange = (newPage) => {
    setSearchParams({ q: query, page: newPage, sort });
  };
  
  return (
    <div>
      <input 
        value={query}
        onChange={e => handleSearch(e.target.value)}
        placeholder="Search..."
      />
      
      <p>Query: {query}</p>
      <p>Page: {page}</p>
      <p>Sort: {sort}</p>
      
      <button onClick={() => handlePageChange('2')}>Page 2</button>
    </div>
  );
}

// URL: /search?q=react&page=2&sort=date

πŸš€ Programmatic Navigation

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

function LoginForm() {
  const navigate = useNavigate();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  
  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      await login(email, password);
      // Navigate to dashboard after login
      navigate('/dashboard');
      
      // Or navigate with replace (no back button)
      // navigate('/dashboard', { replace: true });
      
      // Or go back
      // navigate(-1);
    } catch (error) {
      alert('Login failed');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      <button type="submit">Login</button>
    </form>
  );
}

// Navigate with state
function ProductList() {
  const navigate = useNavigate();
  
  const viewProduct = (product) => {
    navigate(`/products/${product.id}`, {
      state: { product }  // Pass data
    });
  };
  
  return (
    <div>
      {products.map(product => (
        <button key={product.id} onClick={() => viewProduct(product)}>
          {product.name}
        </button>
      ))}
    </div>
  );
}

// Access state with useLocation
import { useLocation } from 'react-router-dom';

function ProductDetail() {
  const location = useLocation();
  const product = location.state?.product;
  
  return <div>{product?.name}</div>;
}

πŸ”’ Protected Routes

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

function ProtectedRoute({ children }) {
  const { user } = useAuth();  // Your auth context
  
  if (!user) {
    // Redirect to login if not authenticated
    return <Navigate to="/login" replace />;
  }
  
  return children;
}

// Usage
function App() {
  return (
    <Routes>
      <Route path="/login" element={<Login />} />
      
      {/* Protected routes */}
      <Route 
        path="/dashboard" 
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        } 
      />
      
      <Route 
        path="/profile" 
        element={
          <ProtectedRoute>
            <Profile />
          </ProtectedRoute>
        } 
      />
    </Routes>
  );
}

// Admin-only route
function AdminRoute({ children }) {
  const { user } = useAuth();
  
  if (!user) {
    return <Navigate to="/login" />;
  }
  
  if (user.role !== 'admin') {
    return <Navigate to="/unauthorized" />;
  }
  
  return children;
}

πŸ—οΈ Nested Routes & Layout

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

// Layout component
function DashboardLayout() {
  return (
    <div className="dashboard">
      <aside>
        <nav>
          <Link to="/dashboard">Overview</Link>
          <Link to="/dashboard/products">Products</Link>
          <Link to="/dashboard/orders">Orders</Link>
          <Link to="/dashboard/settings">Settings</Link>
        </nav>
      </aside>
      
      <main>
        {/* Nested routes render here */}
        <Outlet />
      </main>
    </div>
  );
}

// App with nested routes
function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      
      {/* Parent route with layout */}
      <Route path="/dashboard" element={<DashboardLayout />}>
        {/* Child routes */}
        <Route index element={<DashboardOverview />} />
        <Route path="products" element={<Products />} />
        <Route path="orders" element={<Orders />} />
        <Route path="settings" element={<Settings />} />
      </Route>
    </Routes>
  );
}

// URLs:
// /dashboard β†’ DashboardLayout + DashboardOverview
// /dashboard/products β†’ DashboardLayout + Products
// /dashboard/orders β†’ DashboardLayout + Orders

❌ 404 Page

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/about" element={<About />} />
      
      {/* Catch all unmatched routes */}
      <Route path="*" element={<NotFound />} />
    </Routes>
  );
}

function NotFound() {
  const navigate = useNavigate();
  
  return (
    <div className="not-found">
      <h1>404 - Page Not Found</h1>
      <p>The page you're looking for doesn't exist.</p>
      <button onClick={() => navigate('/')}>Go Home</button>
    </div>
  );
}

🎯 Complete Example: E-commerce App

// App.jsx
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import Products from './pages/Products';
import ProductDetail from './pages/ProductDetail';
import Cart from './pages/Cart';
import Checkout from './pages/Checkout';
import Login from './pages/Login';
import NotFound from './pages/NotFound';

function App() {
  return (
    <div>
      <Navbar />
      
      <Routes>
        {/* Public routes */}
        <Route path="/" element={<Home />} />
        <Route path="/products" element={<Products />} />
        <Route path="/products/:id" element={<ProductDetail />} />
        <Route path="/login" element={<Login />} />
        
        {/* Protected routes */}
        <Route path="/cart" element={<ProtectedRoute><Cart /></ProtectedRoute>} />
        <Route path="/checkout" element={<ProtectedRoute><Checkout /></ProtectedRoute>} />
        
        {/* 404 */}
        <Route path="*" element={<NotFound />} />
      </Routes>
    </div>
  );
}

// Navbar.jsx
import { Link, NavLink } from 'react-router-dom';
import { useCart } from '../context/CartContext';
import { useAuth } from '../context/AuthContext';

function Navbar() {
  const { itemCount } = useCart();
  const { user, logout } = useAuth();
  
  return (
    <nav className="navbar">
      <Link to="/" className="logo">ShopApp</Link>
      
      <div className="nav-links">
        <NavLink to="/">Home</NavLink>
        <NavLink to="/products">Products</NavLink>
        
        {user ? (
          <>
            <NavLink to="/cart">
              Cart ({itemCount})
            </NavLink>
            <button onClick={logout}>Logout</button>
          </>
        ) : (
          <NavLink to="/login">Login</NavLink>
        )}
      </div>
    </nav>
  );
}

// Products.jsx
import { useSearchParams } from 'react-router-dom';

function Products() {
  const [searchParams, setSearchParams] = useSearchParams();
  const category = searchParams.get('category') || 'all';
  const sort = searchParams.get('sort') || 'name';
  
  return (
    <div>
      <h1>Products</h1>
      
      <select 
        value={category}
        onChange={e => setSearchParams({ category: e.target.value, sort })}
      >
        <option value="all">All Categories</option>
        <option value="electronics">Electronics</option>
        <option value="clothing">Clothing</option>
      </select>
      
      {/* Product list */}
    </div>
  );
}

// ProductDetail.jsx
import { useParams, useNavigate } from 'react-router-dom';

function ProductDetail() {
  const { id } = useParams();
  const navigate = useNavigate();
  const [product, setProduct] = useState(null);
  
  useEffect(() => {
    fetch(`/api/products/${id}`)
      .then(res => res.json())
      .then(setProduct)
      .catch(() => navigate('/products'));
  }, [id, navigate]);
  
  if (!product) return <div>Loading...</div>;
  
  return (
    <div>
      <button onClick={() => navigate(-1)}>← Back</button>
      <h1>{product.name}</h1>
      <p>${product.price}</p>
      <button>Add to Cart</button>
    </div>
  );
}

🎯 Key Takeaways