Skip to main content

πŸ—ΊοΈ Lesson 6.1: React Router Basics

Master the fundamentals of client-side routing in React applications with React Router. Learn how to create seamless navigation experiences without full page reloads, building the foundation for modern single-page applications.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Understand what Single Page Applications (SPAs) are and why routing matters
  • Install and configure React Router in your TypeScript project
  • Create multiple routes using BrowserRouter and Routes components
  • Navigate between pages using Link and NavLink components
  • Type route parameters correctly with TypeScript
  • Build a multi-page application with proper navigation structure

Estimated Time: 60-75 minutes

Prerequisites: Modules 1-5 completed (React hooks, TypeScript, component patterns)

πŸ“‘ In This Lesson

🌐 Understanding Single Page Applications

Before diving into React Router, let's understand what Single Page Applications (SPAs) are and why routing is essential for modern web applications.

What is a Single Page Application?

A Single Page Application (SPA) is a web application that loads a single HTML page and dynamically updates content as users interact with the app. Unlike traditional multi-page applications that reload the entire page for each navigation, SPAs update only the parts of the page that change.

graph TD A[User Clicks Link] --> B{App Type?} B -->|Traditional MPA| C[Full Page Reload] B -->|Modern SPA| D[Client-Side Navigation] C --> E[Server Sends New HTML] C --> F[Browser Parses & Renders] C --> G[Slow, Full Refresh] D --> H[Update URL] D --> I[Render New Component] D --> J[Fast, Smooth Transition] style A fill:#667eea,color:#fff style D fill:#48bb78,color:#fff style C fill:#fc8181,color:#fff

Traditional Multi-Page vs Single Page Apps

Aspect Traditional MPA Modern SPA
Navigation Full page reload Instant, no reload
User Experience Slower, jarring transitions Smooth, app-like feel
Server Load Higher (every navigation) Lower (initial load only)
SEO Easier (native) Requires configuration
Initial Load Faster Slower (loads entire app)
State Management Simpler (resets each page) Complex (persists)

πŸ“– Key Concept: Client-Side Routing

Client-Side Routing means the browser URL changes, but no request is sent to the server. Instead, JavaScript intercepts the navigation, updates the URL, and renders the appropriate component. This creates the illusion of multiple pages while actually staying on one page.

Why Do We Need React Router?

React, by itself, doesn't have built-in routing capabilities. When you create a React app without routing, you're limited to a single view or must manually manage which components to show. React Router solves this by providing:

  • πŸ—ΊοΈ URL-based navigation: Different URLs display different components
  • πŸ”— Linkable pages: Users can bookmark and share specific views
  • ⬅️ Browser history: Back and forward buttons work as expected
  • πŸ“± Deep linking: Direct access to any part of your app
  • 🎯 Code organization: Separate concerns by route/page

πŸ’‘ Real-World Example

Think of Netflix: when you click on a movie, the URL changes to something like /watch/12345, but the page doesn't reload. The video player component appears instantly. When you browse back to the home page, it's immediate. That's the power of client-side routing!

How React Router Works

React Router works by:

sequenceDiagram participant U as User participant B as Browser participant RR as React Router participant R as React U->>B: Clicks link B->>RR: URL changes RR->>RR: Matches URL to route RR->>R: Renders matched component R->>B: Updates DOM B->>U: Shows new view Note over RR: No server request!
  1. Listening to URL changes: React Router watches for URL changes using the browser's History API
  2. Matching routes: It compares the current URL against your defined routes
  3. Rendering components: When a match is found, it renders the corresponding component
  4. Managing history: It keeps track of navigation so back/forward buttons work

βœ… Benefits of React Router

  • Declarative: Define routes using JSX components
  • Dynamic: Routes can have parameters and nested structures
  • Type-Safe: Full TypeScript support
  • Flexible: Supports multiple routing strategies
  • Well-Maintained: Industry standard with great documentation

πŸ“¦ Installing and Setting Up React Router

Let's get React Router installed and configured in your TypeScript project.

Installation

React Router v6 is the current version. Install it using npm or yarn:

# Using npm
npm install react-router-dom

# Using yarn
yarn add react-router-dom

# TypeScript types are included automatically in v6+
# No need for @types/react-router-dom anymore!

⚠️ Version Note

This lesson uses React Router v6, which has a different API than v5. If you're working with an older project, be aware that the syntax will differ. Always check which version you're using with npm list react-router-dom.

Project Structure for Routing

Before adding routes, let's organize our project structure. Here's a recommended folder layout:

src/
β”œβ”€β”€ App.tsx                 # Main app component with router setup
β”œβ”€β”€ main.tsx               # Entry point
β”œβ”€β”€ pages/                 # Page components (route destinations)
β”‚   β”œβ”€β”€ Home.tsx
β”‚   β”œβ”€β”€ About.tsx
β”‚   β”œβ”€β”€ Contact.tsx
β”‚   β”œβ”€β”€ NotFound.tsx
β”‚   └── ...
β”œβ”€β”€ components/            # Reusable components
β”‚   β”œβ”€β”€ Navbar.tsx
β”‚   β”œβ”€β”€ Footer.tsx
β”‚   └── ...
β”œβ”€β”€ layouts/              # Layout components
β”‚   └── MainLayout.tsx
└── types/                # TypeScript types
    └── index.ts

πŸ“– Naming Convention

Pages vs Components: We put route components (pages) in a separate pages/ folder to distinguish them from reusable components. This makes it clear which components are tied to specific routes.

Basic Router Setup

The foundation of React Router is the BrowserRouter component. It must wrap your entire application to enable routing functionality.

Update your src/main.tsx (or src/index.tsx):

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

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

πŸ’‘ Why Wrap in main.tsx?

We wrap our app with BrowserRouter at the highest level (entry point) so that routing functionality is available throughout the entire application. Any component, no matter how deeply nested, can use React Router hooks and components.

Alternative: HashRouter

React Router also offers HashRouter as an alternative to BrowserRouter:

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

// URLs will look like: http://example.com/#/about
<HashRouter>
  <App />
</HashRouter>
Router Type URL Format When to Use
BrowserRouter /about Modern apps with server support
HashRouter /#/about Static hosting (GitHub Pages)

βœ… Recommendation

Use BrowserRouter for most projects. It creates cleaner URLs and is the standard for modern web applications. Only use HashRouter if you're deploying to static hosting that doesn't support server-side routing configuration.

πŸ›£οΈ Creating Your First Routes

Now that React Router is installed, let's create some pages and define routes for them.

Creating Page Components

First, let's create simple page components. Create these files in src/pages/:

src/pages/Home.tsx:

import React from 'react';

function Home() {
  return (
    <div>
      <h1>🏠 Welcome Home</h1>
      <p>This is the home page of our application.</p>
      <p>Navigate to other pages using the links above!</p>
    </div>
  );
}

export default Home;

src/pages/About.tsx:

import React from 'react';

function About() {
  return (
    <div>
      <h1>πŸ“– About Us</h1>
      <p>Learn more about our company and mission.</p>
      <ul>
        <li>Founded in 2024</li>
        <li>Building amazing React applications</li>
        <li>Powered by TypeScript</li>
      </ul>
    </div>
  );
}

export default About;

src/pages/Contact.tsx:

import React from 'react';

function Contact() {
  return (
    <div>
      <h1>πŸ“§ Contact Us</h1>
      <p>Get in touch with our team!</p>
      <form>
        <div>
          <label htmlFor="name">Name:</label>
          <input type="text" id="name" name="name" />
        </div>
        <div>
          <label htmlFor="email">Email:</label>
          <input type="email" id="email" name="email" />
        </div>
        <div>
          <label htmlFor="message">Message:</label>
          <textarea id="message" name="message" rows={4}></textarea>
        </div>
        <button type="submit">Send Message</button>
      </form>
    </div>
  );
}

export default Contact;

src/pages/NotFound.tsx:

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

function NotFound() {
  return (
    <div style={{ textAlign: 'center', padding: '3rem' }}>
      <h1>404 - Page Not Found</h1>
      <p>The page you're looking for doesn't exist.</p>
      <Link to="/">← Go back home</Link>
    </div>
  );
}

export default NotFound;

πŸ’‘ Page Component Pattern

Notice that page components are just regular React components. The only difference is their purpose: they represent entire pages/views in your application, not reusable UI pieces.

Defining Routes with Routes and Route

Now let's wire up these pages to URLs. Update your src/App.tsx:

import React from 'react';
import { Routes, Route } 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 className="app">
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
        <Route path="/contact" element={<Contact />} />
        <Route path="*" element={<NotFound />} />
      </Routes>
    </div>
  );
}

export default App;

πŸ“– Understanding the Code

  • <Routes>: Container for all your route definitions
  • <Route>: Defines a single route mapping
  • path: The URL path to match
  • element: The component to render when path matches
  • path="*": Wildcard that matches any unmatched route (404 page)

How Route Matching Works

React Router matches routes from top to bottom and renders the first match it finds:

flowchart TD A[URL: /about] --> B[Check Routes] B --> C[path=/ ?] C -->|No match| D[path=/about ?] D -->|Match!| E[Render About Component] D -->|No match| F[path=/contact ?] F -->|No match| G[path=* ?] G -->|Match!| H[Render NotFound] style E fill:#48bb78,color:#fff style H fill:#fc8181,color:#fff

⚠️ Route Order Matters (Sort of)

In React Router v6, route matching is more sophisticated than v5. The wildcard * route should always come last, but specific routes can be in any order. React Router automatically ranks routes by specificity.

Testing Your Routes

Start your development server and test the routes:

npm run dev
# or
yarn dev

Try navigating to these URLs manually in your browser:

  • http://localhost:5173/ β†’ Should show Home page
  • http://localhost:5173/about β†’ Should show About page
  • http://localhost:5173/contact β†’ Should show Contact page
  • http://localhost:5173/doesnotexist β†’ Should show 404 page

βœ… Success!

If you can manually navigate to these URLs and see the correct pages, your routing is working! However, you'll notice that changing URLs in the address bar causes full page reloads. In the next section, we'll add proper navigation links to avoid this.

πŸ”— Navigation with Link and NavLink

Manually typing URLs in the browser isn't a great user experience. Let's add proper navigation links that work without full page reloads.

The Problem with Regular Anchor Tags

You might be tempted to use regular HTML <a> tags for navigation:

// ❌ DON'T DO THIS
function Navbar() {
  return (
    <nav>
      <a href="/">Home</a>
      <a href="/about">About</a>
      <a href="/contact">Contact</a>
    </nav>
  );
}

The problem: Regular <a> tags cause full page reloads, defeating the purpose of a Single Page Application. Your React app will restart from scratch on every navigation!

⚠️ Why Full Reloads Are Bad

  • Loses all component state
  • Slower user experience
  • Re-downloads JavaScript bundles
  • Triggers unnecessary re-initialization
  • Wastes bandwidth and server resources

Using the Link Component

React Router provides the Link component for client-side navigation:

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

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

export default Navbar;

πŸ“– How Link Works

Link renders as an <a> tag in the DOM, but React Router intercepts the click event, prevents the default behavior, and updates the URL using the History API. This triggers a re-render with the new route component, all without a page reload.

Link Props and Options

The Link component accepts several useful props:

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

function NavigationExample() {
  return (
    <div>
      {/* Basic link */}
      <Link to="/about">About Us</Link>
      
      {/* Link with relative path */}
      <Link to="../parent">Go to Parent</Link>
      
      {/* Link with state (pass data to next page) */}
      <Link 
        to="/profile" 
        state={{ from: 'homepage', userId: 123 }}
      >
        View Profile
      </Link>
      
      {/* Link that replaces history entry */}
      <Link to="/login" replace>
        Login
      </Link>
      
      {/* Link with additional props */}
      <Link 
        to="/docs"
        className="nav-link"
        target="_blank"
        rel="noopener noreferrer"
      >
        Documentation
      </Link>
    </div>
  );
}
Prop Type Description
to string Destination path (required)
replace boolean Replace current history entry instead of pushing
state any Pass data to the destination route
relative string How relative paths are resolved

Introducing NavLink

NavLink is a special version of Link that adds styling to the active link. This is perfect for navigation menus where you want to highlight the current page.

import React from 'react';
import { NavLink } from 'react-router-dom';
import './Navbar.css';

function Navbar() {
  return (
    <nav className="navbar">
      <NavLink 
        to="/"
        className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
      >
        Home
      </NavLink>
      
      <NavLink 
        to="/about"
        className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
      >
        About
      </NavLink>
      
      <NavLink 
        to="/contact"
        className={({ isActive }) => isActive ? 'nav-link active' : 'nav-link'}
      >
        Contact
      </NavLink>
    </nav>
  );
}

export default Navbar;

And the corresponding CSS (Navbar.css):

.navbar {
  display: flex;
  gap: 1rem;
  padding: 1rem;
  background: #667eea;
}

.nav-link {
  color: white;
  text-decoration: none;
  padding: 0.5rem 1rem;
  border-radius: 4px;
  transition: background-color 0.2s;
}

.nav-link:hover {
  background-color: rgba(255, 255, 255, 0.1);
}

.nav-link.active {
  background-color: rgba(255, 255, 255, 0.2);
  font-weight: bold;
}

πŸ’‘ NavLink Features

NavLink provides additional props and features:

  • isActive: Boolean passed to className/style functions
  • isPending: Boolean for loading states (v6.4+)
  • end: Only match exact path (no child routes)
  • caseSensitive: Make path matching case-sensitive

NavLink Style Options

You can style NavLink in multiple ways:

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

function Navbar() {
  return (
    <nav>
      {/* Option 1: className function */}
      <NavLink
        to="/"
        className={({ isActive }) => isActive ? 'active' : ''}
      >
        Home
      </NavLink>
      
      {/* Option 2: style function */}
      <NavLink
        to="/about"
        style={({ isActive }) => ({
          color: isActive ? '#667eea' : '#333',
          fontWeight: isActive ? 'bold' : 'normal'
        })}
      >
        About
      </NavLink>
      
      {/* Option 3: children function (advanced) */}
      <NavLink to="/contact">
        {({ isActive }) => (
          <span>
            {isActive && '➜ '}
            Contact
          </span>
        )}
      </NavLink>
    </nav>
  );
}

Complete Navbar Example

Let's create a full-featured navbar component. Create src/components/Navbar.tsx:

import React from 'react';
import { NavLink } from 'react-router-dom';
import './Navbar.css';

function Navbar() {
  // Helper function to determine active class
  const getNavLinkClass = ({ isActive }: { isActive: boolean }) => {
    return isActive ? 'nav-link nav-link-active' : 'nav-link';
  };
  
  return (
    <nav className="navbar">
      <div className="navbar-brand">
        <NavLink to="/" className="brand-link">
          πŸš€ My App
        </NavLink>
      </div>
      
      <div className="navbar-links">
        <NavLink to="/" end className={getNavLinkClass}>
          Home
        </NavLink>
        <NavLink to="/about" className={getNavLinkClass}>
          About
        </NavLink>
        <NavLink to="/contact" className={getNavLinkClass}>
          Contact
        </NavLink>
      </div>
    </nav>
  );
}

export default Navbar;

βœ… The end Prop

Notice the end prop on the Home link. Without it, / would match all paths (since all paths start with /). The end prop ensures it only matches the exact path.

Integrating Navbar into App

Now let's add the navbar to our application. Update src/App.tsx:

import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
import './App.css';

function App() {
  return (
    <div className="app">
      <Navbar />
      
      <main className="main-content">
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
          <Route path="*" element={<NotFound />} />
        </Routes>
      </main>
    </div>
  );
}

export default App;

Add some basic styling in src/App.css:

.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.main-content {
  flex: 1;
  padding: 2rem;
  max-width: 1200px;
  margin: 0 auto;
  width: 100%;
}

πŸ‹οΈ Exercise: Add a Footer with Links

Create a Footer component that appears on every page with links to Privacy Policy and Terms of Service pages.

πŸ’‘ Hint

Create Footer.tsx in the components folder, use Link components for navigation, and add it to App.tsx below the Routes.

βœ… Solution
// src/components/Footer.tsx
import React from 'react';
import { Link } from 'react-router-dom';
import './Footer.css';

function Footer() {
  return (
    <footer className="footer">
      <div className="footer-content">
        <p>© 2024 My App. All rights reserved.</p>
        <div className="footer-links">
          <Link to="/privacy">Privacy Policy</Link>
          <Link to="/terms">Terms of Service</Link>
        </div>
      </div>
    </footer>
  );
}

export default Footer;

// In App.tsx, add after main:
// <Footer />

πŸ“ Typing Routes with TypeScript

One of the benefits of using React Router with TypeScript is type safety. Let's explore how to properly type your routing code.

Typing Route Parameters

When you have dynamic routes (which we'll cover more in the next lesson), you'll want to type the parameters. Here's a preview:

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

// Define the shape of your route params
interface UserParams {
  userId: string;
}

function UserProfile() {
  // Type the params with your interface
  const { userId } = useParams<UserParams>();
  
  // TypeScript now knows userId is string | undefined
  if (!userId) {
    return <div>User ID is required</div>;
  }
  
  return <div>User Profile: {userId}</div>;
}

export default UserProfile;

πŸ“– Why string | undefined?

Route parameters are always string | undefined because TypeScript can't guarantee the parameter exists at runtime. Even if your route requires it, users can manually type URLs. Always check for undefined!

Creating a Routes Configuration

For larger applications, it's helpful to centralize your route definitions with proper types:

// src/types/routes.ts

export const ROUTES = {
  HOME: '/',
  ABOUT: '/about',
  CONTACT: '/contact',
  PROFILE: '/profile/:userId',
  BLOG: '/blog',
  BLOG_POST: '/blog/:postId',
  NOT_FOUND: '*'
} as const;

// Type for route keys
export type RouteKey = keyof typeof ROUTES;

// Type for route values
export type RoutePath = typeof ROUTES[RouteKey];

Now use these constants throughout your app:

// In App.tsx
import { Routes, Route } from 'react-router-dom';
import { ROUTES } from './types/routes';
import Home from './pages/Home';
import About from './pages/About';
// ... other imports

function App() {
  return (
    <Routes>
      <Route path={ROUTES.HOME} element={<Home />} />
      <Route path={ROUTES.ABOUT} element={<About />} />
      <Route path={ROUTES.CONTACT} element={<Contact />} />
      <Route path={ROUTES.NOT_FOUND} element={<NotFound />} />
    </Routes>
  );
}

// In components
import { Link } from 'react-router-dom';
import { ROUTES } from '../types/routes';

function Navbar() {
  return (
    <nav>
      <Link to={ROUTES.HOME}>Home</Link>
      <Link to={ROUTES.ABOUT}>About</Link>
    </nav>
  );
}

βœ… Benefits of Route Constants

  • Type Safety: TypeScript catches typos at compile time
  • Refactoring: Change a route in one place
  • Autocomplete: IDE suggestions for routes
  • Documentation: All routes visible in one file
  • Consistency: Everyone uses the same paths

Typing Link and NavLink Props

For reusable navigation components, you can type the props:

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

// Extend LinkProps for custom behavior
interface CustomLinkProps extends LinkProps {
  icon?: string;
  badge?: number;
}

function CustomLink({ icon, badge, children, ...linkProps }: CustomLinkProps) {
  return (
    <Link {...linkProps} className="custom-link">
      {icon && <span className="icon">{icon}</span>}
      {children}
      {badge && badge > 0 && (
        <span className="badge">{badge}</span>
      )}
    </Link>
  );
}

// Usage
<CustomLink to="/messages" icon="πŸ“§" badge={5}>
  Messages
</CustomLink>

Typing Router State

When passing state through navigation, type it properly:

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

// Define state shape
interface LocationState {
  from: string;
  userId?: number;
}

// Passing state
function HomePage() {
  return (
    <Link 
      to="/profile"
      state={{ from: 'home', userId: 123 } as LocationState}
    >
      View Profile
    </Link>
  );
}

// Receiving state
function ProfilePage() {
  const location = useLocation();
  const state = location.state as LocationState | null;
  
  return (
    <div>
      <h1>Profile</h1>
      {state && (
        <p>You came from: {state.from}</p>
      )}
    </div>
  );
}

⚠️ Type Assertion with State

Unfortunately, React Router doesn't provide type inference for location state, so you need to use type assertions (as). This is one of the few places where type assertions are appropriate. Always check if state exists before using it!

Creating Type-Safe Route Builders

For dynamic routes, create helper functions:

// src/utils/routes.ts

export const buildUserProfileRoute = (userId: string | number): string => {
  return `/profile/${userId}`;
};

export const buildBlogPostRoute = (postId: string | number): string => {
  return `/blog/${postId}`;
};

// Usage
import { Link } from 'react-router-dom';
import { buildUserProfileRoute } from '../utils/routes';

function UserList({ users }: { users: User[] }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          <Link to={buildUserProfileRoute(user.id)}>
            {user.name}
          </Link>
        </li>
      ))}
    </ul>
  );
}

πŸ‹οΈ Exercise: Create Route Type Definitions

Create a comprehensive routes configuration file with constants for all your routes and helper functions for dynamic routes.

πŸ’‘ Hint

Define an object with all route paths, export it as const, and create builder functions for routes with parameters.

βœ… Solution
// src/config/routes.ts

export const ROUTES = {
  HOME: '/',
  ABOUT: '/about',
  CONTACT: '/contact',
  BLOG: '/blog',
  BLOG_POST: '/blog/:postId',
  USER_PROFILE: '/user/:userId',
  SETTINGS: '/settings',
  NOT_FOUND: '*'
} as const;

export const buildBlogPostRoute = (postId: string | number) => 
  `/blog/${postId}`;

export const buildUserProfileRoute = (userId: string | number) => 
  `/user/${userId}`;

export type RouteKey = keyof typeof ROUTES;

πŸ—οΈ Building a Multi-Page Application

Let's put everything together and build a complete multi-page application with proper routing, navigation, and TypeScript types.

Project Structure

Here's the complete structure we'll build:

src/
β”œβ”€β”€ App.tsx
β”œβ”€β”€ main.tsx
β”œβ”€β”€ config/
β”‚   └── routes.ts           # Route constants
β”œβ”€β”€ components/
β”‚   β”œβ”€β”€ Navbar.tsx          # Navigation component
β”‚   β”œβ”€β”€ Navbar.css
β”‚   β”œβ”€β”€ Footer.tsx          # Footer component
β”‚   └── Footer.css
β”œβ”€β”€ pages/
β”‚   β”œβ”€β”€ Home.tsx
β”‚   β”œβ”€β”€ About.tsx
β”‚   β”œβ”€β”€ Contact.tsx
β”‚   β”œβ”€β”€ Services.tsx        # New page
β”‚   β”œβ”€β”€ Blog.tsx            # New page
β”‚   └── NotFound.tsx
└── types/
    └── index.ts            # Shared types

Step 1: Define Routes Configuration

Create src/config/routes.ts:

export const ROUTES = {
  HOME: '/',
  ABOUT: '/about',
  SERVICES: '/services',
  BLOG: '/blog',
  CONTACT: '/contact',
  NOT_FOUND: '*'
} as const;

export type RouteKey = keyof typeof ROUTES;
export type RoutePath = typeof ROUTES[RouteKey];

Step 2: Create Additional Pages

src/pages/Services.tsx:

import React from 'react';

function Services() {
  const services = [
    { id: 1, name: 'Web Development', icon: 'πŸ’»', description: 'Custom websites and web applications' },
    { id: 2, name: 'Mobile Apps', icon: 'πŸ“±', description: 'iOS and Android development' },
    { id: 3, name: 'UI/UX Design', icon: '🎨', description: 'Beautiful and intuitive interfaces' },
    { id: 4, name: 'Consulting', icon: 'πŸ’Ό', description: 'Technical consulting services' }
  ];

  return (
    <div>
      <h1>πŸ› οΈ Our Services</h1>
      <p>We offer a wide range of professional services to help your business grow.</p>
      
      <div className="services-grid">
        {services.map(service => (
          <div key={service.id} className="service-card">
            <div className="service-icon">{service.icon}</div>
            <h3>{service.name}</h3>
            <p>{service.description}</p>
          </div>
        ))}
      </div>
    </div>
  );
}

export default Services;

src/pages/Blog.tsx:

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

interface BlogPost {
  id: number;
  title: string;
  excerpt: string;
  date: string;
  author: string;
}

function Blog() {
  const posts: BlogPost[] = [
    {
      id: 1,
      title: 'Getting Started with React Router',
      excerpt: 'Learn the basics of routing in React applications...',
      date: '2024-01-15',
      author: 'Jane Doe'
    },
    {
      id: 2,
      title: 'TypeScript Best Practices',
      excerpt: 'Improve your TypeScript code with these tips...',
      date: '2024-01-10',
      author: 'John Smith'
    },
    {
      id: 3,
      title: 'Building SPAs with React',
      excerpt: 'A comprehensive guide to single-page applications...',
      date: '2024-01-05',
      author: 'Jane Doe'
    }
  ];

  return (
    <div>
      <h1>πŸ“ Blog</h1>
      <p>Stay updated with our latest articles and tutorials.</p>
      
      <div className="blog-posts">
        {posts.map(post => (
          <article key={post.id} className="blog-post-card">
            <h2>{post.title}</h2>
            <div className="post-meta">
              <span>By {post.author}</span>
              <span>{new Date(post.date).toLocaleDateString()}</span>
            </div>
            <p>{post.excerpt}</p>
            <Link to={`/blog/${post.id}`}>Read more β†’</Link>
          </article>
        ))}
      </div>
    </div>
  );
}

export default Blog;

Step 3: Enhanced Navbar

Update src/components/Navbar.tsx with all routes:

import React from 'react';
import { NavLink } from 'react-router-dom';
import { ROUTES } from '../config/routes';
import './Navbar.css';

function Navbar() {
  const navLinks = [
    { to: ROUTES.HOME, label: 'Home', end: true },
    { to: ROUTES.ABOUT, label: 'About' },
    { to: ROUTES.SERVICES, label: 'Services' },
    { to: ROUTES.BLOG, label: 'Blog' },
    { to: ROUTES.CONTACT, label: 'Contact' }
  ];

  return (
    <nav className="navbar">
      <div className="navbar-container">
        <NavLink to={ROUTES.HOME} className="navbar-brand">
          πŸš€ My App
        </NavLink>
        
        <div className="navbar-links">
          {navLinks.map(link => (
            <NavLink
              key={link.to}
              to={link.to}
              end={link.end}
              className={({ isActive }) => 
                isActive ? 'nav-link nav-link-active' : 'nav-link'
              }
            >
              {link.label}
            </NavLink>
          ))}
        </div>
      </div>
    </nav>
  );
}

export default Navbar;

Step 4: Complete App Component

Update src/App.tsx with all routes:

import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { ROUTES } from './config/routes';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import Home from './pages/Home';
import About from './pages/About';
import Services from './pages/Services';
import Blog from './pages/Blog';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';
import './App.css';

function App() {
  return (
    <div className="app">
      <Navbar />
      
      <main className="main-content">
        <Routes>
          <Route path={ROUTES.HOME} element={<Home />} />
          <Route path={ROUTES.ABOUT} element={<About />} />
          <Route path={ROUTES.SERVICES} element={<Services />} />
          <Route path={ROUTES.BLOG} element={<Blog />} />
          <Route path={ROUTES.CONTACT} element={<Contact />} />
          <Route path={ROUTES.NOT_FOUND} element={<NotFound />} />
        </Routes>
      </main>
      
      <Footer />
    </div>
  );
}

export default App;

Step 5: Styling

Add comprehensive styling in src/App.css:

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  line-height: 1.6;
  color: #333;
}

.app {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.main-content {
  flex: 1;
  padding: 2rem;
  max-width: 1200px;
  margin: 0 auto;
  width: 100%;
}

/* Services Grid */
.services-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 2rem;
  margin-top: 2rem;
}

.service-card {
  padding: 2rem;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  text-align: center;
  transition: transform 0.2s, box-shadow 0.2s;
}

.service-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.service-icon {
  font-size: 3rem;
  margin-bottom: 1rem;
}

/* Blog Posts */
.blog-posts {
  display: flex;
  flex-direction: column;
  gap: 2rem;
  margin-top: 2rem;
}

.blog-post-card {
  padding: 1.5rem;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
}

.blog-post-card h2 {
  margin-bottom: 0.5rem;
  color: #667eea;
}

.post-meta {
  display: flex;
  gap: 1rem;
  font-size: 0.875rem;
  color: #666;
  margin-bottom: 1rem;
}

.blog-post-card a {
  color: #667eea;
  text-decoration: none;
  font-weight: 500;
}

.blog-post-card a:hover {
  text-decoration: underline;
}

@media (max-width: 768px) {
  .main-content {
    padding: 1rem;
  }
  
  .services-grid {
    grid-template-columns: 1fr;
  }
}

βœ… What We've Built

You now have a fully functional multi-page application with:

  • βœ… Five different pages with unique content
  • βœ… Type-safe route configuration
  • βœ… Active link highlighting in navigation
  • βœ… Proper application structure
  • βœ… Responsive design
  • βœ… 404 error handling

πŸ‹οΈ Exercise: Add a Products Page

Create a new Products page that displays a list of products. Add it to your routes configuration and navigation menu.

πŸ’‘ Requirements
  • Create Products.tsx in pages/
  • Display at least 4 products with name, price, image
  • Add PRODUCTS route to routes.ts
  • Add Products link to Navbar
  • Add Products route to App.tsx
βœ… Solution Outline
// 1. Update routes.ts
export const ROUTES = {
  // ... existing routes
  PRODUCTS: '/products',
  // ...
};

// 2. Create Products.tsx
const products = [
  { id: 1, name: 'Product 1', price: 29.99, image: '🎁' },
  // ... more products
];

// 3. Add to Navbar.tsx navLinks array
{ to: ROUTES.PRODUCTS, label: 'Products' }

// 4. Add to App.tsx Routes
<Route path={ROUTES.PRODUCTS} element={<Products />} />

✨ Best Practices and Common Patterns

Let's explore some best practices and common patterns when working with React Router to build maintainable and scalable applications.

1. Organize Routes by Feature

As your application grows, organize routes by feature rather than keeping everything in one file:

src/
β”œβ”€β”€ features/
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ routes.tsx        # Auth-related routes
β”‚   β”‚   β”œβ”€β”€ Login.tsx
β”‚   β”‚   └── Register.tsx
β”‚   β”œβ”€β”€ blog/
β”‚   β”‚   β”œβ”€β”€ routes.tsx        # Blog-related routes
β”‚   β”‚   β”œβ”€β”€ BlogList.tsx
β”‚   β”‚   └── BlogPost.tsx
β”‚   └── dashboard/
β”‚       β”œβ”€β”€ routes.tsx        # Dashboard routes
β”‚       └── Dashboard.tsx
└── App.tsx                   # Main app with route imports

Example feature routes file (features/blog/routes.tsx):

import { Route } from 'react-router-dom';
import BlogList from './BlogList';
import BlogPost from './BlogPost';

export const blogRoutes = (
  <>
    <Route path="/blog" element={<BlogList />} />
    <Route path="/blog/:postId" element={<BlogPost />} />
  </>
);

// In App.tsx
import { Routes } from 'react-router-dom';
import { blogRoutes } from './features/blog/routes';
import { authRoutes } from './features/auth/routes';

function App() {
  return (
    <Routes>
      {blogRoutes}
      {authRoutes}
      {/* ... other routes */}
    </Routes>
  );
}

βœ… Benefits of Feature-Based Routes

  • Scalability: Easy to add new features without cluttering App.tsx
  • Maintainability: Routes live with related components
  • Team Collaboration: Different teams can work on different features
  • Code Splitting: Easier to implement lazy loading per feature

2. Use Index Routes for Default Content

When you have nested routes, use index routes to specify default content:

import { Routes, Route } from 'react-router-dom';
import Dashboard from './Dashboard';
import Overview from './Overview';
import Settings from './Settings';

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={<Dashboard />}>
        {/* Index route - shown at /dashboard */}
        <Route index element={<Overview />} />
        {/* Child routes */}
        <Route path="settings" element={<Settings />} />
      </Route>
    </Routes>
  );
}

πŸ“– Index Routes

An index route renders when the parent route matches exactly. It's like a default child route. We'll explore nested routes more in Lesson 6.2.

3. Create a Custom 404 Component

Make your 404 page helpful and on-brand:

import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import './NotFound.css';

function NotFound() {
  const location = useLocation();
  
  return (
    <div className="not-found-container">
      <div className="not-found-content">
        <h1 className="not-found-title">404</h1>
        <h2>Page Not Found</h2>
        <p>
          The page <code>{location.pathname}</code> doesn't exist.
        </p>
        
        <div className="not-found-actions">
          <Link to="/" className="button button-primary">
            Go to Homepage
          </Link>
          <button 
            onClick={() => window.history.back()}
            className="button button-secondary"
          >
            Go Back
          </button>
        </div>
        
        <div className="helpful-links">
          <h3>Popular Pages:</h3>
          <ul>
            <li><Link to="/about">About Us</Link></li>
            <li><Link to="/contact">Contact</Link></li>
            <li><Link to="/blog">Blog</Link></li>
          </ul>
        </div>
      </div>
    </div>
  );
}

export default NotFound;

4. Loading States for Route Transitions

Show feedback during route changes (especially important with lazy loading):

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

// Lazy load components
const Home = React.lazy(() => import('./pages/Home'));
const About = React.lazy(() => import('./pages/About'));

function LoadingSpinner() {
  return (
    <div className="loading-container">
      <div className="spinner"></div>
      <p>Loading...</p>
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  );
}

πŸ’‘ Why Lazy Loading?

Lazy loading (code splitting) loads route components only when needed, reducing initial bundle size. This makes your app load faster, especially important for users on slow connections.

5. Scroll to Top on Route Change

By default, React Router doesn't scroll to the top when navigating. Create a component to handle this:

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.tsx
function App() {
  return (
    <div className="app">
      <ScrollToTop />
      <Navbar />
      <Routes>
        {/* ... routes */}
      </Routes>
    </div>
  );
}

6. Use URL Parameters for Shareable State

Put important state in the URL so users can bookmark and share specific views:

// βœ… GOOD: State in URL
// URL: /products?category=electronics&sort=price&page=2

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

function Products() {
  const [searchParams, setSearchParams] = useSearchParams();
  
  const category = searchParams.get('category') || 'all';
  const sort = searchParams.get('sort') || 'name';
  const page = searchParams.get('page') || '1';
  
  const handleCategoryChange = (newCategory: string) => {
    setSearchParams({ category: newCategory, sort, page });
  };
  
  // Users can bookmark this URL and return to exact state!
  return (
    <div>
      <select value={category} onChange={e => handleCategoryChange(e.target.value)}>
        <option value="all">All</option>
        <option value="electronics">Electronics</option>
      </select>
      {/* ... */}
    </div>
  );
}
// ❌ BAD: State only in React
// URL: /products (no context)

import { useState } from 'react';

function Products() {
  const [category, setCategory] = useState('all');
  
  // State is lost on refresh!
  // Can't share this view with others!
}

⚠️ When to Use URL vs Local State

Use URL for: Filters, search queries, pagination, sort order - anything users might want to bookmark or share.

Use local state for: UI-only state like modals, tooltips, form inputs (before submission).

7. Breadcrumb Navigation

Create breadcrumbs to help users understand their location:

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

function Breadcrumbs() {
  const location = useLocation();
  
  const pathnames = location.pathname.split('/').filter(x => x);
  
  return (
    <nav className="breadcrumbs">
      <Link to="/">Home</Link>
      {pathnames.map((name, index) => {
        const routeTo = `/${pathnames.slice(0, index + 1).join('/')}`;
        const isLast = index === pathnames.length - 1;
        
        return isLast ? (
          <span key={routeTo}> / {name}</span>
        ) : (
          <span key={routeTo}>
            {' / '}
            <Link to={routeTo}>{name}</Link>
          </span>
        );
      })}
    </nav>
  );
}

// Usage: Renders "Home / blog / post-title" for /blog/post-title

8. Route Configuration Array Pattern

For complex routing, use a configuration array:

import { RouteObject, useRoutes } from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import Contact from './pages/Contact';

const routes: RouteObject[] = [
  {
    path: '/',
    element: <Home />,
  },
  {
    path: '/about',
    element: <About />,
  },
  {
    path: '/contact',
    element: <Contact />,
  },
  {
    path: '*',
    element: <NotFound />,
  }
];

function App() {
  const element = useRoutes(routes);
  return (
    <div className="app">
      <Navbar />
      {element}
    </div>
  );
}

βœ… Best Practices Summary

  • Organize routes by feature for scalability
  • Use index routes for default nested content
  • Create helpful 404 pages with navigation options
  • Implement loading states for better UX
  • Scroll to top on route changes
  • Store shareable state in URL parameters
  • Add breadcrumbs for complex navigation
  • Use route configuration arrays for complex apps
  • Keep route constants in a central location
  • Type everything with TypeScript

Common Pitfalls to Avoid

⚠️ Don't Use <a> Tags

Always use <Link> or <NavLink> for internal navigation. Regular <a> tags cause full page reloads.

⚠️ Don't Forget the Wildcard Route

Always include a path="*" route for 404 handling. Place it last in your route definitions.

⚠️ Don't Nest BrowserRouter

Only wrap your app once with <BrowserRouter> at the top level. Don't add additional routers in child components.

⚠️ Don't Store Everything in URL

Not all state belongs in the URL. Keep form inputs, modal states, and temporary UI state in local component state.

πŸ‹οΈ Exercise: Implement Best Practices

Enhance your multi-page app with three best practices from this section.

πŸ’‘ Suggested Improvements
  1. Add a ScrollToTop component
  2. Enhance the NotFound page with helpful links
  3. Add breadcrumb navigation
βœ… Implementation Tips
  • ScrollToTop: Create component, import useLocation and useEffect
  • NotFound: Use useLocation to show attempted path, add navigation options
  • Breadcrumbs: Parse location.pathname, create links for each segment

🎯 Summary and Next Steps

Congratulations! You've learned the fundamentals of React Router and built your first multi-page application.

What You've Learned

πŸŽ“ Key Concepts Mastered

  • SPAs: Understanding single-page applications and client-side routing
  • Installation: Setting up React Router with TypeScript
  • Routes: Creating routes with BrowserRouter, Routes, and Route
  • Navigation: Using Link and NavLink for client-side navigation
  • TypeScript: Typing routes, parameters, and navigation state
  • Structure: Organizing a multi-page application
  • Best Practices: Professional patterns and common pitfalls

Complete Example: Everything Together

Here's a complete, production-ready routing setup:

// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';

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

// src/config/routes.ts
export const ROUTES = {
  HOME: '/',
  ABOUT: '/about',
  SERVICES: '/services',
  BLOG: '/blog',
  CONTACT: '/contact',
  NOT_FOUND: '*'
} as const;

// src/App.tsx
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import { ROUTES } from './config/routes';
import Navbar from './components/Navbar';
import Footer from './components/Footer';
import ScrollToTop from './components/ScrollToTop';
import Home from './pages/Home';
import About from './pages/About';
import Services from './pages/Services';
import Blog from './pages/Blog';
import Contact from './pages/Contact';
import NotFound from './pages/NotFound';

function App() {
  return (
    <div className="app">
      <ScrollToTop />
      <Navbar />
      <main className="main-content">
        <Routes>
          <Route path={ROUTES.HOME} element={<Home />} />
          <Route path={ROUTES.ABOUT} element={<About />} />
          <Route path={ROUTES.SERVICES} element={<Services />} />
          <Route path={ROUTES.BLOG} element={<Blog />} />
          <Route path={ROUTES.CONTACT} element={<Contact />} />
          <Route path={ROUTES.NOT_FOUND} element={<NotFound />} />
        </Routes>
      </main>
      <Footer />
    </div>
  );
}

export default App;

Quick Reference: React Router Essentials

Component/Hook Purpose Example
BrowserRouter Wrap app for routing <BrowserRouter><App /></BrowserRouter>
Routes Container for routes <Routes>...</Routes>
Route Define a route <Route path="/" element={<Home />} />
Link Navigate to route <Link to="/about">About</Link>
NavLink Link with active state <NavLink to="/">Home</NavLink>
useLocation Get current location const location = useLocation()
useNavigate Programmatic navigation navigate('/about')

What's Next?

In the upcoming lessons, you'll learn:

πŸ“š Lesson 6.2: Advanced Routing

  • Nested routes and layouts
  • Dynamic route parameters
  • useParams and useNavigate hooks
  • Programmatic navigation
  • useLocation hook in depth

πŸ“š Lesson 6.3: Route Protection and Loading

  • Protected/private routes
  • Authentication flows
  • Lazy loading with React.lazy and Suspense
  • Code splitting strategies
  • Error boundaries with routing

πŸ“š Lesson 6.4: Search and Query Parameters

  • useSearchParams hook
  • Managing query strings
  • Filters and search in URL
  • Pagination with URL state

Practice Projects

To reinforce your learning, try building:

  1. Portfolio Website: Multi-page portfolio with About, Projects, Contact pages
  2. Documentation Site: Navigation between different doc sections
  3. Restaurant Menu: Different pages for menu categories
  4. Product Catalog: Browse different product categories

Additional Resources

πŸŽ‰ Congratulations!

You've mastered React Router basics!

You can now build multi-page React applications with professional routing.

πŸ‹οΈ Final Challenge: Build a Complete Site

Create a complete website with:

  • At least 5 different pages
  • Navigation menu with active link highlighting
  • Footer with additional links
  • Custom 404 page
  • ScrollToTop functionality
  • Route constants in a config file
  • TypeScript types for all routes
  • Responsive design

Suggested Theme: Personal blog, business website, or hobby showcase