Now that we have state and components, let’s connect everything with navigation. Most apps aren’t a single page—they have multiple views (Home, Tasks, Settings). We’ll set up React Router with TypeScript and keep it beginner-friendly.
🎯 What we’ll build
- A layout with a nav and nested routes
- Static & dynamic routes (e.g.,
/tasks/:id) - Lazy-loaded pages (faster first load)
- 404 Not Found page
- A protected route pattern for authenticated pages
- Type-safe helpers for links
1) Install
pnpm add react-router-dom
React Router ships its own TypeScript types; no extra
@typesneeded.
2) Folder plan
src/
app/
AppRouter.tsx # router config
Layout.tsx # shared layout (nav + <Outlet/>)
pages/
home/Home.tsx
tasks/Tasks.tsx
tasks/TaskDetails.tsx
settings/Settings.tsx
auth/Login.tsx
errors/NotFound.tsx
3) Layout with <Outlet /> (shared UI)
// src/app/Layout.tsx
import { NavLink, Outlet } from 'react-router-dom';
import { Header } from '@/components/Header';
export default function Layout() {
return (
<div className="min-h-screen">
{/* Site-wide header */}
<Header title="TaskTimer" rightSlot={<span>⏱️</span>} />
{/* Simple nav; NavLink adds active state */}
<nav className="p-4 flex gap-4 border-b">
<NavLink to="/" className={({ isActive }) => (isActive ? 'font-bold' : '')}>Home</NavLink>
<NavLink to="/tasks" className={({ isActive }) => (isActive ? 'font-bold' : '')}>Tasks</NavLink>
<NavLink to="/settings" className={({ isActive }) => (isActive ? 'font-bold' : '')}>Settings</NavLink>
</nav>
{/* Nested routes render here */}
<main className="p-4">
<Outlet />
</main>
</div>
);
}
4) Pages (simple starters)
// src/pages/home/Home.tsx
export default function Home() {
return (
<section>
<h2 className="text-xl font-semibold mb-2">Welcome 👋</h2>
<p>This is your TaskTimer home. Use the nav above to explore.</p>
</section>
);
}
// src/pages/tasks/Tasks.tsx
import { Link } from 'react-router-dom';
type Task = { id: string; title: string; done: boolean };
const sample: Task[] = [
{ id: '1', title: 'Buy milk', done: false },
{ id: '2', title: 'Write blog', done: true },
];
export default function Tasks() {
return (
<section>
<h2 className="text-xl font-semibold mb-2">Tasks</h2>
<ul>
{sample.map((t) => (
<li key={t.id}>
{/* Link to dynamic route */}
<Link to={`/tasks/${t.id}`}>{t.title}</Link>
</li>
))}
</ul>
</section>
);
}
// src/pages/tasks/TaskDetails.tsx
import { useParams } from 'react-router-dom';
type Params = { id: string };
export default function TaskDetails() {
// Typed params ensure we always read a string id
const { id } = useParams<Params>();
return (
<section>
<h2 className="text-xl font-semibold mb-2">Task Details</h2>
<p>Task ID: {id}</p>
</section>
);
}
// src/pages/settings/Settings.tsx
export default function Settings() {
return <h2 className="text-xl font-semibold">Settings</h2>;
}
// src/pages/auth/Login.tsx
import { useNavigate } from 'react-router-dom';
export default function Login() {
const navigate = useNavigate();
const login = () => {
// TODO: perform auth; then redirect
navigate('/settings');
};
return (
<div>
<h2 className="text-xl font-semibold mb-2">Login</h2>
<button onClick={login}>Sign in</button>
</div>
);
}
// src/pages/errors/NotFound.tsx
export default function NotFound() {
return <h2 className="text-xl">❌ Page not found</h2>;
}
5) Lazy-loaded routes (faster initial load)
// src/app/AppRouter.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { Suspense, lazy } from 'react';
import Layout from './Layout';
import NotFound from '@/pages/errors/NotFound';
// Lazy imports; code-split per page
const Home = lazy(() => import('@/pages/home/Home'));
const Tasks = lazy(() => import('@/pages/tasks/Tasks'));
const TaskDetails = lazy(() => import('@/pages/tasks/TaskDetails'));
const Settings = lazy(() => import('@/pages/settings/Settings'));
const Login = lazy(() => import('@/pages/auth/Login'));
export default function AppRouter() {
return (
<BrowserRouter>
{/* Suspense shows a fallback while a lazy page loads */}
<Suspense fallback={<p className="p-4">Loading…</p>}>
<Routes>
<Route element={<Layout />}> {/* shared layout */}
<Route path="/" element={<Home />} />
<Route path="/tasks" element={<Tasks />} />
{/* dynamic route with ":id" param */}
<Route path="/tasks/:id" element={<TaskDetails />} />
{/* protected route example below */}
<Route path="/settings" element={<RequireAuth><Settings /></RequireAuth>} />
</Route>
{/* auth + 404 */}
<Route path="/login" element={<Login />} />
<Route path="*" element={<NotFound />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
6) Protected route pattern (beginner-friendly)
// src/app/RequireAuth.tsx
import type { ReactNode } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
// Pretend auth state (replace with real auth later)
const isAuthenticated = () => Boolean(localStorage.getItem('auth:token'));
export default function RequireAuth({ children }: { children: ReactNode }) {
const location = useLocation();
if (!isAuthenticated()) {
// Redirect to /login and remember where we wanted to go
return <Navigate to="/login" state={{ from: location }} replace />;
}
return <>{children}</>;
}
Use it like:
<Route path="/settings" element={<RequireAuth><Settings /></RequireAuth>} />
7) Type-safe route helpers (avoid hardcoded strings)
// src/app/routes.ts
export const routes = {
home: '/',
tasks: '/tasks',
task: (id: string) => `/tasks/${id}`,
settings: '/settings',
login: '/login',
} as const;
// example usage
import { Link } from 'react-router-dom';
import { routes } from '@/app/routes';
<Link to={routes.tasks}>Tasks</Link>
<Link to={routes.task('123')}>Open Task 123</Link>
8) Wire router in main
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import AppRouter from '@/app/AppRouter';
import './index.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AppRouter />
</React.StrictMode>
);
✅ Wrap-Up
In Part 5, you learned how to:
- Create a layout with nested routes
- Add static, dynamic (
:id), and lazy-loaded routes - Build a friendly 404 page
- Implement a simple protected route wrapper
- Centralize routes for type-safe links
👉 Next: We’ll build Forms Like a Pro using React Hook Form + Zod for typed validation and great UX.
Comments
Post a Comment