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.
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
/dashboardsection 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'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'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
dashboardroute is a child of the root route, so its URL is/dashboard. - Within
dashboard, we define an index route (overview), aprojectslist route, and aprojects/:projectIdroute. - Because
ProjectsPageusesLink 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
/dashboardwith child routes - A dashboard layout route with its own sidebar and content outlet
- URL parameters via
useParamsonprojects/: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
Related Articles
Routing in React with React Router and TypeScript – Part 5: Architecture and Best Practices
Bring everything together with a production-ready architecture for React Router + TypeScript apps built with Vite. Learn about feature-first structure, lazy routes, testing, and common pitfalls.
Routing in React with React Router and TypeScript – Part 4: Auth and Protected Routes
Add authentication and protected routes to your React Router + TypeScript app. We’ll build a simple auth flow with login, logout, and role-based access inside the Vite-powered dashboard.
Full Stack Insights
Software Engineer
Passionate about software development, architecture, and sharing knowledge.
Quick Links
Full Stack Insights
Software Engineer
Passionate about software development, architecture, and sharing knowledge with the community.