πΊοΈ 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.
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:
- Listening to URL changes: React Router watches for URL changes using the browser's History API
- Matching routes: It compares the current URL against your defined routes
- Rendering components: When a match is found, it renders the corresponding component
- 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 mappingpath: The URL path to matchelement: The component to render when path matchespath="*": 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:
β οΈ 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 pagehttp://localhost:5173/aboutβ Should show About pagehttp://localhost:5173/contactβ Should show Contact pagehttp://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 functionsisPending: 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
- Add a ScrollToTop component
- Enhance the NotFound page with helpful links
- 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:
- Portfolio Website: Multi-page portfolio with About, Projects, Contact pages
- Documentation Site: Navigation between different doc sections
- Restaurant Menu: Different pages for menu categories
- Product Catalog: Browse different product categories
Additional Resources
- π React Router Official Docs
- π React Router GitHub
- π₯ React Training YouTube
- π¬ Remix/React Router Discord
π 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