Skip to main content

🧱 Part 5 — Routing with React Router (Pages, Params, Lazy, 404, Protected) 🧭

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 @types needed.


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

Popular posts from this blog

🌟 Dot net Microservices interview questions

Here is a comprehensive list of 200 .NET microservices coding questions covering all core microservices concepts and cross-cutting concerns relevant for designing, building, deploying, and maintaining .NET-based distributed systems. 🧩 A. Microservices Fundamentals (20) Build a microservice in .NET 8 that exposes a simple CRUD API. Implement communication between two microservices using REST. How would you design microservices for an e-commerce application? Create a microservice that handles user registration and login. How do you isolate domain logic in a microservice? How to apply the "Single Responsibility Principle" in microservices? Design a service registry/discovery mechanism using custom middleware. Implement a service that handles file uploads and metadata separately. Build a stateless microservice and explain its benefits. Implement health check endpoints in .NET 8. Demonstrate versioning in a microservice API. Add Swagger/OpenAPI support to your m...

⚡ Part 1: Introduction to Generics in C#

🌍 Why Do We Need Generics? Imagine you want to create a stack (like a pile of books 📚): You can push items on top You can pop items off the top If we write a stack for integers : public class IntStack { private int[] items = new int[10]; private int index = 0; public void Push(int item) => items[index++] = item; public int Pop() => items[--index]; } 👉 Problem: This only works for int . What if we want a string stack ? Or a Customer stack ? We’d have to duplicate code for every type. 😢 ✅ Solution: Generics Generics let us create type-safe reusable code without duplication. We can say: “I don’t care what type it is yet — I’ll decide later.” 1) Generic Classes Here’s a generic stack : // Generic class "Stack<T>" // The <T> is a placeholder for any type public class Stack<T> { private T[] items = new T[10]; // Array of type T private int index = 0; // Push adds an item of type T public void P...

🚪 Part 9: API Gateway for .NET 8 Microservices (Ocelot & YARP)

Once you have multiple microservices (Products, Orders, Payments…), exposing each one directly to clients gets messy: Different base URLs Duplicated auth logic No unified rate limiting / caching Hard to evolve routes or aggregate data 👉 Enter the API Gateway — your single front door for all microservices. An API Gateway handles: ✅ Routing & path rewriting ✅ Load balancing, retries, circuit breakers ✅ Authentication & Authorization (JWT, OAuth2) ✅ Rate limiting & caching ✅ Aggregation (compose results from multiple services) In this post we’ll implement two strong options: Ocelot → config-driven, mature, DevOps-friendly YARP (Yet Another Reverse Proxy) → Microsoft’s code-first, extensible gateway ⚖️ Ocelot vs YARP — When to Choose Ocelot → JSON config, minimal C#, built-in QoS (rate limit, circuit breaker). Perfect for teams that like DevOps config-as-code. YARP → full C# control, middleware-friendly, can embed into broader apps (e.g. add dashb...