DEV_NET_CORE
GET_STARTED
ReactRouting, forms, and server communication

Nested routes, layouts, params, and route boundaries

Overview

Nested routes, layouts, params, and route boundaries are central to building React applications where the URL reflects the shape of the UI. Instead of treating routing as a flat list of pages, modern routing libraries such as React Router let routes form a tree. Parent routes can provide layout, navigation, shared data, error handling, and loading boundaries while child routes render the specific screen for the current URL.

Code
const router = createBrowserRouter([
  {
    path: "/dashboard",
    Component: DashboardLayout,
    children: [
      { index: true, Component: DashboardHome },
      { path: "settings", Component: SettingsPage },
      { path: "teams/:teamId", Component: TeamPage },
    ],
  },
]);

In this example:

  • /dashboard renders DashboardLayout and DashboardHome.
  • /dashboard/settings renders DashboardLayout and SettingsPage.
  • /dashboard/teams/42 renders DashboardLayout and TeamPage with teamId available as a route param.

This topic matters for interviews because routing reveals whether a developer understands more than navigation links. Strong React developers should be able to design route trees, explain layout routes and outlets, use params correctly, avoid route ambiguity, place error/loading boundaries thoughtfully, and understand how route-level boundaries affect data loading and resilience.

The practical goal is to make the URL, data requirements, layout hierarchy, and error isolation match the user's mental model of the application.

Core Concepts

Route Trees

A route tree describes which UI should render for a URL.

Code
const router = createBrowserRouter([
  {
    path: "/",
    Component: RootLayout,
    children: [
      { index: true, Component: HomePage },
      { path: "about", Component: AboutPage },
      {
        path: "projects",
        Component: ProjectsLayout,
        children: [
          { index: true, Component: ProjectsIndex },
          { path: ":projectId", Component: ProjectDetails },
        ],
      },
    ],
  },
]);

This route tree maps to a UI tree. Parent routes stay mounted while child routes change, which is useful for persistent navigation, shared layout, tabs, sidebars, and common data.

Route trees help avoid duplicating page shells:

Code
function RootLayout() {
  return (
    <>
      <Header />
      <main>
        <Outlet />
      </main>
    </>
  );
}

The Outlet is where the matching child route renders.

Nested Routes

Nested routes are child routes declared under a parent route.

Code
{
  path: "/account",
  Component: AccountLayout,
  children: [
    { index: true, Component: AccountOverview },
    { path: "billing", Component: BillingPage },
    { path: "security", Component: SecurityPage },
  ],
}

The child paths are relative to the parent:

  • /account
  • /account/billing
  • /account/security

The parent component must render an Outlet for children to appear.

Code
function AccountLayout() {
  return (
    <section>
      <AccountNav />
      <Outlet />
    </section>
  );
}

If the parent does not render Outlet, the child route can match but the child UI has nowhere to render.

Layout Routes

A layout route provides UI structure without necessarily adding a URL segment.

Code
{
  Component: MarketingLayout,
  children: [
    { index: true, Component: LandingPage },
    { path: "pricing", Component: PricingPage },
    { path: "contact", Component: ContactPage },
  ],
}

Because the parent route has no path, it does not add a URL segment. It only wraps its children in shared layout.

Layout routes are useful for:

  • Marketing pages.
  • Auth pages.
  • App shell vs public shell.
  • Admin sections.
  • Repeated navigation and sidebars.
  • Shared error boundaries.

Example with multiple layout layers:

Code
{
  path: "/app",
  Component: AppLayout,
  children: [
    {
      Component: SettingsLayout,
      children: [
        { path: "profile", Component: ProfileSettings },
        { path: "billing", Component: BillingSettings },
      ],
    },
  ],
}

The URL /app/profile can render both AppLayout and SettingsLayout even if SettingsLayout does not add its own segment.

Index Routes

An index route is the default child route for a parent URL.

Code
{
  path: "/dashboard",
  Component: DashboardLayout,
  children: [
    { index: true, Component: DashboardHome },
    { path: "reports", Component: ReportsPage },
  ],
}

When the user visits /dashboard, the index route renders inside DashboardLayout.

Index routes:

  • Do not have a path.
  • Render at the parent route's URL.
  • Cannot have child routes.
  • Are useful for default content.

Common mistake:

Code
{ path: "", Component: DashboardHome }

Prefer index: true because it clearly communicates "default child route."

Prefix Routes

A prefix route groups child routes under a URL path without adding a layout component.

Code
{
  path: "/projects",
  children: [
    { index: true, Component: ProjectsHome },
    { path: ":projectId", Component: ProjectDetails },
    { path: ":projectId/edit", Component: EditProject },
  ],
}

This creates URL paths under /projects, but no parent component is rendered. Use this when you want path grouping without shared UI.

Use a layout route when shared UI or an Outlet is needed. Use a prefix route when only the URL grouping matters.

Dynamic Params

A dynamic segment starts with :.

Code
{
  path: "teams/:teamId",
  Component: TeamPage,
}

For /teams/42, teamId is "42".

Components can read params:

Code
function TeamPage() {
  const params = useParams();

  return <h1>Team {params.teamId}</h1>;
}

Loaders and actions can also receive params:

Code
{
  path: "teams/:teamId",
  loader: async ({ params }) => {
    return fetchTeam(params.teamId);
  },
  Component: TeamPage,
}

Params are strings. Convert and validate them when needed:

Code
const teamId = Number(params.teamId);

if (!Number.isInteger(teamId)) {
  throw new Error("Invalid team id");
}

Do not assume route params are trusted or valid just because the route matched.

Nested Params

Child routes inherit params from parent routes.

Code
{
  path: "organizations/:orgId",
  Component: OrganizationLayout,
  children: [
    { path: "projects/:projectId", Component: ProjectPage },
  ],
}

At /organizations/acme/projects/123, the project route can access both:

Code
const { orgId, projectId } = useParams();

This is useful when child data depends on parent identity:

Code
async function loader({ params }: LoaderArgs) {
  return fetchProject({
    orgId: params.orgId,
    projectId: params.projectId,
  });
}

Avoid reusing the same param name at multiple levels because it creates confusion:

Code
// Risky: both parent and child use :id.
"/organizations/:id/projects/:id"

Prefer meaningful names:

Code
"/organizations/:orgId/projects/:projectId"

Search Params vs Route Params

Route params identify path segments:

Code
/products/42

42 is usually a route param such as productId.

Search params represent query-string state:

Code
/products?sort=price&page=2

sort and page are search params.

Use route params for:

  • Entity identity.
  • Hierarchical resources.
  • Canonical page identity.

Use search params for:

  • Filters.
  • Sort order.
  • Pagination.
  • Search text.
  • View options.

Good design:

Code
/teams/:teamId/members?role=admin&page=2

The team identity is part of the path. Filtering and pagination are query-string state.

Optional Segments and Splats

Some routing systems support optional path segments and splats.

Optional segment:

Code
{ path: ":lang?/categories", Component: CategoriesPage }

This can match both:

  • /categories
  • /en/categories

Splat route:

Code
{ path: "files/*", Component: FileBrowser }

A splat catches the remaining path. It is useful for file paths, documentation paths, and catch-all screens.

Be careful with broad splats. Put specific routes before broad catch-all concepts in your mental model, and keep 404 behavior intentional.

Route Boundaries

A route boundary is a place where routing behavior is isolated. It can be a layout boundary, data boundary, loading boundary, or error boundary.

Examples:

  • A parent route layout that keeps sidebar state while children change.
  • A loader boundary that fetches data for a section.
  • An error boundary that catches route errors below it.
  • A lazy route boundary that code-splits a feature area.
  • A reset boundary where changing a route key resets local state.

Route boundaries make large apps more resilient. A failure in /invoices/:invoiceId should not necessarily crash the whole app shell. An error boundary near the invoice route can show a focused failure screen while preserving the broader layout.

Error Boundaries

Route-level error boundaries render when a route component, loader, action, or related route API throws.

Code
const router = createBrowserRouter([
  {
    path: "/app",
    Component: AppLayout,
    ErrorBoundary: AppErrorBoundary,
    children: [
      {
        path: "invoices/:invoiceId",
        Component: InvoicePage,
        ErrorBoundary: InvoiceErrorBoundary,
      },
    ],
  },
]);

If InvoicePage or its loader throws, the nearest boundary is InvoiceErrorBoundary. If a route has no boundary, the error bubbles to the nearest parent boundary.

Use route error boundaries for:

  • 404s from route loaders.
  • Failed route data requirements.
  • Unexpected route rendering errors.
  • Keeping layout shells alive when a child route fails.

Do not use error boundaries for normal form validation. Validation errors should usually be rendered through action data or component state.

Route-Level Code Splitting

Routes are natural code-splitting boundaries. A user who never opens the admin area should not necessarily download all admin screens on the first page load.

Data routers can lazy-load route modules:

Code
{
  path: "/admin",
  lazy: async () => {
    const module = await import("./routes/admin");

    return {
      Component: module.AdminPage,
      loader: module.loader,
    };
  },
}

Good route splitting:

  • Splits large feature areas.
  • Avoids excessive tiny chunks.
  • Keeps route data and route UI colocated when possible.
  • Provides pending UI for navigation.

Use router-aware links instead of raw anchors for client-side navigation.

Code
import { Link, NavLink } from "react-router";

function MainNav() {
  return (
    <nav>
      <NavLink to="/dashboard">Dashboard</NavLink>
      <NavLink to="/dashboard/settings">Settings</NavLink>
    </nav>
  );
}

Link navigates without a full page reload. NavLink can expose active or pending states for navigation styling.

Use normal <a> for external URLs or when intentionally leaving the app.

Designing Route Trees

A good route tree reflects product structure.

For an app with projects:

Code
{
  path: "/projects",
  Component: ProjectsLayout,
  children: [
    { index: true, Component: ProjectList },
    {
      path: ":projectId",
      Component: ProjectLayout,
      children: [
        { index: true, Component: ProjectOverview },
        { path: "settings", Component: ProjectSettings },
        { path: "members", Component: ProjectMembers },
      ],
    },
  ],
}

This supports:

  • Persistent project navigation.
  • Project-level params.
  • Project-level data loading.
  • Project-level error boundary.
  • Child screens for sections.

Avoid huge flat route lists when the UI has obvious hierarchy. Avoid deeply nested routes when the UI does not actually share layout or data.

Common Mistakes

Common mistakes include:

  • Forgetting to render Outlet in a parent route.
  • Using nested routes when a flat route would be clearer.
  • Using flat routes and duplicating layout everywhere.
  • Treating params as numbers without conversion.
  • Reusing generic param names like :id at several levels.
  • Putting filters and pagination in path params instead of search params.
  • Using broad splat routes that hide real 404 behavior.
  • Placing error boundaries only at the root.
  • Using normal <a> tags for internal navigation and causing full page reloads.
  • Creating route trees that do not match the product's actual information architecture.

Best Practices

Use these rules of thumb:

  • Let the route tree mirror the UI and product hierarchy.
  • Use layouts for shared shell, navigation, and persistent UI.
  • Use Outlet intentionally at parent routes.
  • Use index routes for default child content.
  • Use meaningful param names such as orgId and projectId.
  • Validate and convert params before using them in data calls.
  • Use search params for filters, sorting, and pagination.
  • Place error boundaries where failures should be isolated.
  • Use route boundaries for code splitting and data ownership.
  • Keep route definitions understandable enough to reason about navigation flow.

Interview Practice

PreviousForms, validation, optimistic updates, and mutation statesNext UpRoute loaders/actions or equivalent data-loading patterns