useEffect - Side Effects, Data Fetching & Async State
Browser DevTools - The Network Tab
Your portfolio is currently a single page. Everything lives in one place, and if you want to show your projects or contact form, you either scroll down or toggle visibility with state. That works for a simple page… but real applications have multiple distinct views that users expect to navigate between, bookmark, and share as links.
This is what routing solves. React Router is the most widely used routing library for React, and the patterns you learn here will serve you directly when you migrate to Next.js in week 11 — the concepts are the same, only the mechanism changes.
Traditional websites make a new request to the server every time you navigate. Click a link to /projects, the browser asks the server for that page, the server sends back new HTML, and the whole page reloads.
Client-side routing works differently. The browser loads your React application once, and JavaScript intercepts navigation events to show different components without ever making a full page request. The URL updates, the back button works, bookmarks work — but there's no reload and no round trip to the server.
React Router manages this entirely in the browser. It watches the URL and renders the component that matches the current path.
Install React Router:
npm install react-router
Then wrap your application in a BrowserRouter in main.tsx. This gives every component in your app access to React Router's features:
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router';
import App from './App.tsx';
import './index.css';
createRoot(document.getElementById('root')!).render(
<StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>
);
Routes live in your App.tsx. You define them using Routes and Route, telling React Router which component to render for each URL path:
import { Routes, Route } from 'react-router';
import { Navigation } from './components/Navigation';
import { HomePage } from './pages/HomePage';
import { ProjectsPage } from './pages/ProjectsPage';
import { ContactPage } from './pages/ContactPage';
import { NotFoundPage } from './pages/NotFoundPage';
export default function App() {
return (
<>
<Navigation />
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
</>
);
}
Routes looks at the current URL and renders the first Route whose path matches. The path="*" at the end is a catch-all: it renders when nothing else matches, giving users a proper 404 page rather than a blank screen.
Notice that <Navigation /> sits outside <Routes>. This means it stays mounted across all pages: no re-renders, no flickering, just persistent UI that's always there.
Keep your page components in a pages/ folder to distinguish them from reusable UI components:
src/
├── components/
│ ├── Navigation.tsx
│ └── ProjectCard.tsx
├── pages/
│ ├── HomePage.tsx
│ ├── ProjectsPage.tsx
│ ├── ContactPage.tsx
│ └── NotFoundPage.tsx
└── App.tsx
A page component is just a regular React component:
// src/pages/ProjectsPage.tsx
export function ProjectsPage() {
return (
<div>
<h1>Projects</h1>
<p>Here's what I've been working on.</p>
</div>
);
}
Never use a plain <a href> to navigate between pages in a React Router app. A regular anchor tag triggers a full page reload, downloading the JavaScript bundle again and discarding all current state. That defeats the entire purpose of client-side routing.
Use the <Link> component instead:
import { Link } from 'react-router';
export function Navigation() {
return (
<nav>
<Link to="/">Home</Link>
<Link to="/projects">Projects</Link>
<Link to="/contact">Contact</Link>
</nav>
);
}
Link renders as an <a> tag in the HTML — so it's fully accessible, keyboard-navigable, and works with screen readers — but intercepts the click and handles navigation without a page reload.
It's good UX to visually indicate which page the user is currently on. NavLink is a version of Link that knows whether its destination is the active route:
import { NavLink } from 'react-router';
export function Navigation() {
return (
<nav className="flex gap-6">
<NavLink
to="/"
className={({ isActive }) =>
isActive
? 'font-semibold text-blue-600 underline'
: 'text-gray-600 hover:text-gray-900'
}
>
Home
</NavLink>
<NavLink
to="/projects"
className={({ isActive }) =>
isActive
? 'font-semibold text-blue-600 underline'
: 'text-gray-600 hover:text-gray-900'
}
>
Projects
</NavLink>
<NavLink
to="/contact"
className={({ isActive }) =>
isActive
? 'font-semibold text-blue-600 underline'
: 'text-gray-600 hover:text-gray-900'
}
>
Contact
</NavLink>
</nav>
);
}
The className prop on NavLink accepts a function that receives { isActive } — a boolean that's true when the current URL matches this link's to path. You return whichever class string applies.
<aside> 💡
Info
To avoid repeating the same className logic on every NavLink, extract it into a helper:
const navLinkClass = ({ isActive }: { isActive: boolean }) =>
isActive
? 'font-semibold text-blue-600 underline'
: 'text-gray-600 hover:text-gray-900';
<NavLink to="/" className={navLinkClass}>Home</NavLink>
<NavLink to="/projects" className={navLinkClass}>Projects</NavLink>
</aside>
Sometimes you need to navigate in response to an event rather than a click — after a form submission, after an action completes, or based on some condition. The useNavigate hook gives you a function you can call anywhere in your component:
import { useNavigate } from 'react-router';
export function ContactPage() {
const navigate = useNavigate();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
// handle the form data...
navigate('/');
};
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
<button type="submit">Send message</button>
</form>
);
}
navigate('/') pushes a new entry onto the browser history, so the user can click back to return to where they were. navigate(-1) navigates back one step — the equivalent of clicking the browser's back button.
<aside> ⚠️
Warning
Don't call useNavigate outside of a component, and don't call it during render. It belongs in event handlers and callbacks — places where something has happened and you want to send the user somewhere as a result.
</aside>
Sometimes part of the URL is dynamic — a project ID, a blog post slug, a user profile name. Rather than creating a separate route for every possible value, you define a dynamic segment using : in the path:
<Route path="/projects/:projectId" element={<ProjectDetailPage />} />
The :projectId part matches anything. /projects/42, /projects/portfolio-site, /projects/anything — all of these match this route.
Read the value inside the component with useParams:
import { useParams } from 'react-router';
export function ProjectDetailPage() {
const { projectId } = useParams();
return (
<div>
<h1>Project {projectId}</h1>
</div>
);
}
A more realistic example — looking up a project from a list:
import { useParams, Link } from 'react-router';
const projects = [
{ id: '1', title: 'Portfolio Site', description: 'Built with React and Tailwind.' },
{ id: '2', title: 'Weather App', description: 'Fetches live data from the OpenWeather API.' },
{ id: '3', title: 'Task Manager', description: 'Drag and drop task management.' },
];
export function ProjectDetailPage() {
const { projectId } = useParams();
const project = projects.find((p) => p.id === projectId);
if (!project) {
return (
<div>
<h1>Project not found</h1>
<Link to="/projects">Back to projects</Link>
</div>
);
}
return (
<div>
<h1>{project.title}</h1>
<p>{project.description}</p>
<Link to="/projects">← Back to projects</Link>
</div>
);
}
Then on your projects page, link to each detail page:
import { Link } from 'react-router';
export function ProjectsPage() {
return (
<div>
<h1>Projects</h1>
<ul>
{projects.map((project) => (
<li key={project.id}>
<Link to={`/projects/${project.id}`}>
{project.title}
</Link>
</li>
))}
</ul>
</div>
);
}
<aside> ⚠️
Warning
useParams always returns strings, even when the value looks like a number. If you need to use the ID as a number — to index into an array or compare with a numeric ID from an API — convert it explicitly: Number(projectId) or parseInt(projectId, 10).
</aside>
pages/ folder — they're distinct from reusable UI components<Routes> so it doesn't unmount between page changes<Link> or <NavLink> for internal navigation — never <a href>useNavigate for programmatic navigation after events like form submissionspath="*" catch-all route so users see something helpful instead of a blank pageThe HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

Built with ❤️ by the HackYourFuture community · Thank you, contributors
Found a mistake or have a suggestion? Let us know in the feedback form.