Single-Page Applications (SPAs)

A Single-Page Application (SPA) is a web application that loads a single HTML page and dynamically updates content as the user interacts with the app. React Router enables you to create SPAs with multiple "pages" that feel like traditional websites.

Traditional Multi-Page App

🔄 How it works:

  1. User clicks a link
  2. Browser makes new HTTP request
  3. Server sends complete new HTML page
  4. Page refreshes entirely
  5. JavaScript/CSS reload

❌ Disadvantages:

  • Slower navigation
  • Full page reloads
  • State is lost between pages
  • More server requests
Single-Page Application

⚡ How it works:

  1. User clicks a link
  2. JavaScript updates the URL
  3. React Router renders new component
  4. Only content area changes
  5. No page refresh needed

✅ Advantages:

  • Faster navigation
  • Smooth transitions
  • State preservation
  • Better user experience

React Router Setup

React Router is the most popular routing library for React applications. It provides declarative routing that keeps your UI in sync with the URL.

📦 Installation and Basic Setup

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

# Or with yarn
yarn add react-router-dom
Basic Router Setup
import React from 'react';
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';

// Page Components
function Home() {
  return (
    <div>
      <h1>Home Page</h1>
      <p>Welcome to our React application!</p>
    </div>
  );
}

function About() {
  return (
    <div>
      <h1>About Page</h1>
      <p>Learn more about our company.</p>
    </div>
  );
}

function Contact() {
  return (
    <div>
      <h1>Contact Page</h1>
      <p>Get in touch with us!</p>
    </div>
  );
}

// Navigation Component
function Navigation() {
  return (
    <nav>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/contact">Contact</Link></li>
      </ul>
    </nav>
  );
}

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

export default App;

Dynamic Routes and Parameters

Dynamic routes allow you to create flexible URLs that accept parameters. This is essential for pages like user profiles, product details, or blog posts.

🔗 URL Parameters

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

// User Profile Component
function UserProfile() {
  const { userId } = useParams(); // Get URL parameter
  const navigate = useNavigate(); // Programmatic navigation
  
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Fetch user data based on userId
    const fetchUser = async () => {
      try {
        const response = await fetch(`/api/users/${userId}`);
        const userData = await response.json();
        setUser(userData);
      } catch (error) {
        console.error('Error fetching user:', error);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, [userId]);

  if (loading) return <div>Loading user...</div>;
  if (!user) return <div>User not found</div>;

  return (
    <div>
      <button onClick={() => navigate('/users')}>
        ← Back to Users
      </button>
      
      <h1>{user.name}</h1>
      <p>Email: {user.email}</p>
      <p>Joined: {user.joinDate}</p>
      
      <button onClick={() => navigate(`/users/${userId}/edit`)}>
        Edit Profile
      </button>
    </div>
  );
}

// Product Details with Multiple Parameters
function ProductDetails() {
  const { category, productId } = useParams();
  
  return (
    <div>
      <h1>Product Details</h1>
      <p>Category: {category}</p>
      <p>Product ID: {productId}</p>
    </div>
  );
}

// App with Dynamic Routes
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/users" element={<UserList />} />
        <Route path="/users/:userId" element={<UserProfile />} />
        <Route path="/users/:userId/edit" element={<EditProfile />} />
        <Route path="/products/:category/:productId" element={<ProductDetails />} />
        <Route path="*" element={<NotFound />} /> {/* Catch-all route */}
      </Routes>
    </BrowserRouter>
  );
}

Nested Routes

Nested routes allow you to build complex layouts where parts of the UI remain constant while other parts change based on the URL.

🏗️ Building Nested Layouts

Nested Routes Example
import { Outlet, NavLink } from 'react-router-dom';

// Dashboard Layout Component
function Dashboard() {
  return (
    <div className="dashboard">
      <aside className="sidebar">
        <h2>Dashboard</h2>
        <nav>
          <NavLink 
            to="/dashboard" 
            end
            className={({ isActive }) => isActive ? 'active' : ''}
          >
            Overview
          </NavLink>
          
          <NavLink 
            to="/dashboard/profile"
            className={({ isActive }) => isActive ? 'active' : ''}
          >
            Profile
          </NavLink>
          
          <NavLink 
            to="/dashboard/settings"
            className={({ isActive }) => isActive ? 'active' : ''}
          >
            Settings
          </NavLink>
          
          <NavLink 
            to="/dashboard/analytics"
            className={({ isActive }) => isActive ? 'active' : ''}
          >
            Analytics
          </NavLink>
        </nav>
      </aside>
      
      <main className="main-content">
        {/* Nested routes will render here */}
        <Outlet />
      </main>
    </div>
  );
}

// Dashboard Child Components
function DashboardOverview() {
  return (
    <div>
      <h1>Dashboard Overview</h1>
      <div className="stats-grid">
        <div className="stat-card">
          <h3>Total Users</h3>
          <p>1,234</p>
        </div>
        <div className="stat-card">
          <h3>Revenue</h3>
          <p>$12,345</p>
        </div>
      </div>
    </div>
  );
}

function DashboardProfile() {
  return (
    <div>
      <h1>Profile Settings</h1>
      <form>
        <input type="text" placeholder="Name" />
        <input type="email" placeholder="Email" />
        <button type="submit">Save Changes</button>
      </form>
    </div>
  );
}

function DashboardSettings() {
  return (
    <div>
      <h1>Application Settings</h1>
      <div>
        <label>
          <input type="checkbox" /> Enable Notifications
        </label>
        <label>
          <input type="checkbox" /> Dark Mode
        </label>
      </div>
    </div>
  );
}

// App with Nested Routes
function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/login" element={<Login />} />
        
        {/* Nested Dashboard Routes */}
        <Route path="/dashboard" element={<Dashboard />}>
          <Route index element={<DashboardOverview />} /> {/* Default child */}
          <Route path="profile" element={<DashboardProfile />} />
          <Route path="settings" element={<DashboardSettings />} />
          <Route path="analytics" element={<DashboardAnalytics />} />
        </Route>
        
        <Route path="*" element={<NotFound />} />
      </Routes>
    </BrowserRouter>
  );
}

Navigation and Router Hooks

🧭 useNavigate

Programmatically navigate to different routes

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

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

  const handleLogin = async (credentials) => {
    try {
      await login(credentials);
      // Navigate to dashboard after successful login
      navigate('/dashboard');
      
      // Or navigate with replacement (can't go back)
      navigate('/dashboard', { replace: true });
      
      // Navigate back
      navigate(-1);
      
      // Navigate with state
      navigate('/dashboard', { 
        state: { from: '/login' } 
      });
    } catch (error) {
      console.error('Login failed');
    }
  };

  return (
    <form onSubmit={handleLogin}>
      {/* form fields */}
    </form>
  );
}

📍 useLocation

Access current location and state

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

function CurrentPage() {
  const location = useLocation();

  useEffect(() => {
    // Track page views
    analytics.track('Page View', {
      path: location.pathname,
      search: location.search
    });
  }, [location]);

  // Access state passed from navigate
  const fromPage = location.state?.from;

  return (
    <div>
      <p>Current path: {location.pathname}</p>
      <p>Query string: {location.search}</p>
      {fromPage && <p>Came from: {fromPage}</p>}
    </div>
  );
}

🔍 useSearchParams

Read and update URL query parameters

useSearchParams
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, newSort) => {
    setSearchParams({
      category: newCategory,
      sort: newSort,
      page: '1' // Reset to page 1
    });
  };

  return (
    <div>
      <select 
        value={category}
        onChange={(e) => updateFilters(e.target.value, sort)}
      >
        <option value="all">All Categories</option>
        <option value="electronics">Electronics</option>
      </select>
      
      <select 
        value={sort}
        onChange={(e) => updateFilters(category, e.target.value)}
      >
        <option value="name">Sort by Name</option>
        <option value="price">Sort by Price</option>
      </select>
      
      {/* URL will be: /products?category=electronics&sort=price&page=1 */}
    </div>
  );
}

🛡️ Protected Routes

Restrict access based on authentication

Protected Routes
import { Navigate, useLocation } from 'react-router-dom';

function ProtectedRoute({ children }) {
  const { user } = useAuth(); // Custom auth hook
  const location = useLocation();

  if (!user) {
    // Redirect to login with return path
    return (
      <Navigate 
        to="/login" 
        state={{ from: location.pathname }}
        replace 
      />
    );
  }

  return children;
}

// Usage in App
function App() {
  return (
    <Routes>
      <Route path="/login" element={<Login />} />
      
      <Route 
        path="/dashboard" 
        element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        } 
      />
    </Routes>
  );
}

Routing Best Practices

✅ Route Organization

  • Use clear, descriptive URLs
  • Group related routes together
  • Implement consistent naming patterns
  • Use nested routes for layouts
  • Always include a 404 catch-all route

🚀 Performance

  • Use React.lazy for code splitting
  • Implement loading states
  • Preload critical routes
  • Use suspense boundaries
  • Cache route data when appropriate

🛡️ Security

  • Validate route parameters
  • Implement proper authentication
  • Use protected route components
  • Sanitize URL parameters
  • Handle unauthorized access gracefully

♿ Accessibility

  • Update page titles on route changes
  • Manage focus after navigation
  • Provide skip navigation links
  • Use semantic navigation elements
  • Support keyboard navigation

What's Next?

You now understand how to build single-page applications with React Router. Next, you'll explore the React ecosystem and essential development tools.

🚀 Continue Learning

  1. Development Tools - Essential tools for React development
  2. Next.js - Full-stack React framework with built-in routing
  3. Popular Libraries - UI libraries and ecosystem tools
  4. Transition Guide - Complete migration strategies