Routing in React with React Router and TypeScript – Part 2: Nested Routes and Layout Patterns

Learn how to organize nested routes, layout routes, and dashboard-style sections in a React Router app built with Vite and TypeScript. We’ll build a realistic dashboard with shared layout and child pages.

FSI
Full Stack Insights
7 min read...

In Part 1, we set up a Vite + React + TypeScript project and wired React Router with a simple layout and three pages.

In Part 2, we’ll make our routing structure feel more like a real-world application by introducing:

  • A /dashboard section with nested child routes
  • A dedicated dashboard layout route that wraps its children
  • Navigation that reflects both top-level and section-level pages
  • A clean folder structure that scales as features grow

Our goal is to keep the app small but structurally similar to what you’d use in production.

This article assumes you’ve completed Part 1 and have the base project running.


1. What are nested and layout routes?

In React Router, a layout route is a route that renders some UI (like a header or sidebar) and uses an <Outlet /> to render its child routes.

A nested route is defined as a child of another route. It doesn’t need to repeat shared UI; it just provides the content for a portion of the page.

In Part 1, AppLayout acted as a layout route at the root level:

  • / → Home page
  • /about → About page
  • /contact → Contact page

Now we’ll add a new section:

  • /dashboard → Dashboard overview
  • /dashboard/projects → List of projects
  • /dashboard/projects/:projectId → Project details

All of these will share a dashboard shell with its own sidebar navigation.


2. Create the dashboard layout route

We’ll follow the same pattern as AppLayout, but scoped to dashboard.

Create a new file src/routes/dashboard/DashboardLayout.tsx:

import { NavLink, Outlet } from "react-router-dom";
import "./DashboardLayout.css";

export const DashboardLayout: React.FC = () => {
  return (
    <div className="dashboard-layout">
      <aside className="dashboard-sidebar">
        <h2 className="dashboard-sidebar__title">Dashboard</h2>
        <nav className="dashboard-sidebar__nav">
          <NavLink
            to="."
            end
            className={({ isActive }) =>
              isActive
                ? "dashboard-link dashboard-link--active"
                : "dashboard-link"
            }
          >
            Overview
          </NavLink>
          <NavLink
            to="projects"
            className={({ isActive }) =>
              isActive
                ? "dashboard-link dashboard-link--active"
                : "dashboard-link"
            }
          >
            Projects
          </NavLink>
        </nav>
      </aside>
      <section className="dashboard-content">
        <Outlet />
      </section>
    </div>
  );
};

Add basic styling in src/routes/dashboard/DashboardLayout.css:

.dashboard-layout {
  display: grid;
  grid-template-columns: 220px 1fr;
  gap: 1.5rem;
}

.dashboard-sidebar {
  background: #111827;
  color: #e5e7eb;
  padding: 1rem;
  border-radius: 0.5rem;
}

.dashboard-sidebar__title {
  font-size: 1.1rem;
  font-weight: 600;
  margin-bottom: 1rem;
}

.dashboard-sidebar__nav {
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.dashboard-link {
  color: #e5e7eb;
  text-decoration: none;
  padding: 0.35rem 0.5rem;
  border-radius: 0.25rem;
  font-size: 0.95rem;
}

.dashboard-link--active {
  background: #2563eb;
}

.dashboard-content {
  padding: 1rem;
  background: #f3f4f6;
  border-radius: 0.5rem;
}

The key piece is the <Outlet /> – that’s where dashboard child routes like the overview and projects pages will render.


3. Add dashboard child pages

Create src/routes/dashboard/DashboardOverviewPage.tsx:

export const DashboardOverviewPage: React.FC = () => {
  return (
    <section>
      <h1>Dashboard Overview</h1>
      <p>
        This is a simple overview of your product or business metrics.
        In a real application, this could show charts, KPIs, and summaries
        of what&apos;s happening in your system.
      </p>
    </section>
  );
};

Create src/routes/dashboard/ProjectsPage.tsx:

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

const PROJECTS = [
  { id: "p1", name: "Customer Portal Redesign", status: "In Progress" },
  { id: "p2", name: "Internal Tools Revamp", status: "Planning" },
  { id: "p3", name: "Mobile App Launch", status: "Completed" },
];

export const ProjectsPage: React.FC = () => {
  return (
    <section>
      <h1>Projects</h1>
      <p>Here is a small, static list of demo projects.</p>
      <ul>
        {PROJECTS.map((project) => (
          <li key={project.id}>
            <Link to={project.id}>
              {project.name}{project.status}
            </Link>
          </li>
        ))}
      </ul>
    </section>
  );
};

Create src/routes/dashboard/ProjectDetailsPage.tsx:

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

const PROJECTS = [
  { id: "p1", name: "Customer Portal Redesign", status: "In Progress" },
  { id: "p2", name: "Internal Tools Revamp", status: "Planning" },
  { id: "p3", name: "Mobile App Launch", status: "Completed" },
];

export const ProjectDetailsPage: React.FC = () => {
  const { projectId } = useParams();

  const project = PROJECTS.find((p) => p.id === projectId);

  if (!project) {
    return (
      <section>
        <h1>Project Not Found</h1>
        <p>We couldn&apos;t find a project with id "{projectId}".</p>
      </section>
    );
  }

  return (
    <section>
      <h1>{project.name}</h1>
      <p>Status: {project.status}</p>
      <p>
        In a real system, this page would be backed by an API and show
        richer details like owner, due dates, tasks, and activity.
      </p>
    </section>
  );
};

Note the use of useParams to read projectId from the URL.


4. Wire dashboard routes into the router

Now that we have the layout and pages, we need to connect them to the router.

Update src/router.tsx to add the dashboard route tree:

import { createBrowserRouter } from "react-router-dom";
import { AppLayout } from "./routes/AppLayout";
import { HomePage } from "./routes/HomePage";
import { AboutPage } from "./routes/AboutPage";
import { ContactPage } from "./routes/ContactPage";
import { NotFoundPage } from "./routes/NotFoundPage";
import { DashboardLayout } from "./routes/dashboard/DashboardLayout";
import { DashboardOverviewPage } from "./routes/dashboard/DashboardOverviewPage";
import { ProjectsPage } from "./routes/dashboard/ProjectsPage";
import { ProjectDetailsPage } from "./routes/dashboard/ProjectDetailsPage";

export const router = createBrowserRouter([
  {
    path: "/",
    element: <AppLayout />,
    errorElement: <NotFoundPage />,
    children: [
      { index: true, element: <HomePage /> },
      { path: "about", element: <AboutPage /> },
      { path: "contact", element: <ContactPage /> },
      {
        path: "dashboard",
        element: <DashboardLayout />,
        children: [
          { index: true, element: <DashboardOverviewPage /> },
          { path: "projects", element: <ProjectsPage /> },
          { path: "projects/:projectId", element: <ProjectDetailsPage /> },
        ],
      },
    ],
  },
]);

A few important details:

  • The dashboard route is a child of the root route, so its URL is /dashboard.
  • Within dashboard, we define an index route (overview), a projects list route, and a projects/:projectId route.
  • Because ProjectsPage uses Link to={project.id}, the final URLs are like /dashboard/projects/p1.

Restart your dev server if needed and try visiting:

  • /dashboard → overview
  • /dashboard/projects → list
  • /dashboard/projects/p1 → project details

5. Update main navigation for the dashboard

To make the dashboard discoverable from the top-level nav, update src/shared/MainNav.tsx to add a link:

import { NavLink } from "react-router-dom";
import "./MainNav.css";

export const MainNav: React.FC = () => {
  return (
    <header className="main-nav">
      <div className="main-nav__brand">My React Router App</div>
      <nav className="main-nav__links">
        <NavLink
          to="/"
          end
          className={({ isActive }) =>
            isActive ? "main-nav__link main-nav__link--active" : "main-nav__link"
          }
        >
          Home
        </NavLink>
        <NavLink
          to="/about"
          className={({ isActive }) =>
            isActive ? "main-nav__link main-nav__link--active" : "main-nav__link"
          }
        >
          About
        </NavLink>
        <NavLink
          to="/contact"
          className={({ isActive }) =>
            isActive ? "main-nav__link main-nav__link--active" : "main-nav__link"
          }
        >
          Contact
        </NavLink>
        <NavLink
          to="/dashboard"
          className={({ isActive }) =>
            isActive ? "main-nav__link main-nav__link--active" : "main-nav__link"
          }
        >
          Dashboard
        </NavLink>
      </nav>
    </header>
  );
};

Now your app has a global entry point into the dashboard section.


6. Folder structure and scaling this pattern

By now, your src folder might look roughly like this:

src/
  main.tsx
  router.tsx
  index.css
  routes/
    AppLayout.tsx
    HomePage.tsx
    AboutPage.tsx
    ContactPage.tsx
    NotFoundPage.tsx
    dashboard/
      DashboardLayout.tsx
      DashboardLayout.css
      DashboardOverviewPage.tsx
      ProjectsPage.tsx
      ProjectDetailsPage.tsx
  shared/
    MainNav.tsx
    MainNav.css

This keeps:

  • Top-level routes visible at a glance in routes/.
  • Feature-specific routes grouped in folders like routes/dashboard.
  • Cross-cutting UI elements (like the main nav) under shared/.

In a larger app, you might promote dashboard into its own feature folder with its own hooks, services, and components, but the routing structure would stay similar.


7. Recap and what’s next

You now have:

  • A nested route tree for /dashboard with child routes
  • A dashboard layout route with its own sidebar and content outlet
  • URL parameters via useParams on projects/:projectId
  • A clearer folder structure that mirrors your routing hierarchy

In Part 3, we'll make the app more realistic by:

  • Loading data via route loaders instead of in components
  • Handling loading and error states at route level
  • Adding a simple CRUD-style flow (list, create, edit) for items in the dashboard
Share this article:

Related Articles

FSI

Full Stack Insights

Software Engineer

Passionate about software development, architecture, and sharing knowledge with the community.