Skip to main content
Technology & EngineeringRemix208 lines

Routing

Nested routes, dynamic segments, outlets, and file-based routing conventions in Remix

Quick Summary25 lines
You are an expert in Remix routing, including nested routes, dynamic segments, layout routes, pathless routes, and the outlet pattern.

## Key Points

- Keep layout routes thin — they should handle shared chrome (nav, sidebar) and render `<Outlet />`, not business logic.
- Use `_index.tsx` for every parent route so there is always a default child to render.
- Prefer relative `<Link to="...">` inside nested routes to avoid hard-coding parent paths.
- Use `useMatches()` to access data from any active route in the hierarchy without prop drilling.
- Use the `handle` export on routes to pass metadata (breadcrumbs, titles) up the route tree.
- Forgetting `<Outlet />` in a parent route — the child content will never appear.
- Confusing `.` (nesting separator) with `/` (not valid in flat route filenames).
- Using `$` in a route filename but forgetting the param name when accessing `useParams()`.
- Placing a file at `app/routes/dashboard.tsx` and `app/routes/dashboard._index.tsx` but omitting the `<Outlet />` in the dashboard file, so the index child never renders.
- Not returning a response from a loader in a layout route — even if the layout has no data needs, omitting the loader is fine, but returning `undefined` from an existing loader is not.

## Quick Example

```
app/routes/
  _auth.tsx          -> layout only (no URL segment)
  _auth.login.tsx    -> /login
  _auth.register.tsx -> /register
```
skilldb get remix-skills/RoutingFull skill: 208 lines
Paste into your CLAUDE.md or agent config

Routing — Remix

You are an expert in Remix routing, including nested routes, dynamic segments, layout routes, pathless routes, and the outlet pattern.

Overview

Remix uses a file-based routing convention where the file structure inside app/routes/ maps directly to URL segments. Routes can be nested, meaning a parent route renders an <Outlet /> where its child route content appears. This enables shared layouts, persistent UI, and efficient data loading scoped to each segment.

Core Concepts

File-Based Route Convention

Remix v2 uses flat file routing by default. Route segments are separated by dots in the filename:

app/routes/
  _index.tsx            -> /
  about.tsx             -> /about
  concerts._index.tsx   -> /concerts
  concerts.$id.tsx      -> /concerts/:id
  concerts.trending.tsx -> /concerts/trending

Nested Routes and Outlets

A parent route renders an <Outlet /> component where its children appear:

// app/routes/concerts.tsx — layout route for /concerts/*
import { Outlet } from "@remix-run/react";

export default function ConcertsLayout() {
  return (
    <div>
      <h1>Concerts</h1>
      <nav>
        <Link to="">Featured</Link>
        <Link to="trending">Trending</Link>
      </nav>
      <Outlet />
    </div>
  );
}

Dynamic Segments

Prefix a filename segment with $ to capture a dynamic parameter:

// app/routes/concerts.$id.tsx
import { useParams } from "@remix-run/react";

export default function Concert() {
  const { id } = useParams();
  return <div>Concert ID: {id}</div>;
}

Pathless Layout Routes

Use an underscore prefix to create a layout that does not add a URL segment:

app/routes/
  _auth.tsx          -> layout only (no URL segment)
  _auth.login.tsx    -> /login
  _auth.register.tsx -> /register
// app/routes/_auth.tsx
import { Outlet } from "@remix-run/react";

export default function AuthLayout() {
  return (
    <div className="auth-container">
      <Outlet />
    </div>
  );
}

Splat Routes

A $.tsx file catches all remaining URL segments:

// app/routes/files.$.tsx  -> /files/path/to/anything
import { useParams } from "@remix-run/react";

export default function FileCatchAll() {
  const params = useParams();
  const filePath = params["*"]; // "path/to/anything"
  return <p>File: {filePath}</p>;
}

Implementation Patterns

Shared Navigation with Nested Outlets

// app/root.tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "@remix-run/react";

export default function Root() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
      </head>
      <body>
        <header><GlobalNav /></header>
        <main>
          <Outlet />
        </main>
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

Programmatic Navigation

import { useNavigate } from "@remix-run/react";

function SearchForm() {
  const navigate = useNavigate();

  function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const formData = new FormData(event.currentTarget);
    const query = formData.get("q");
    navigate(`/search?q=${query}`);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="q" type="search" />
    </form>
  );
}

Route Configuration with v2_routeConvention

If you need custom route config, use routes in remix.config.js:

// remix.config.js
export default {
  routes(defineRoutes) {
    return defineRoutes((route) => {
      route("/dashboard", "routes/dashboard/layout.tsx", () => {
        route("", "routes/dashboard/index.tsx", { index: true });
        route("settings", "routes/dashboard/settings.tsx");
      });
    });
  },
};

Core Philosophy

Remix routing is fundamentally about nesting, not just URL mapping. The nested route tree is a UI composition model: parent routes render persistent layouts (navigation, sidebars, headers) and child routes render the variable content within those layouts. This mirrors how users actually experience applications, where some elements persist across pages while the central content changes. The <Outlet /> component makes this relationship explicit in code.

The flat file convention, where dots separate URL segments in filenames, is designed to make the route tree scannable in a file listing. You can look at the app/routes/ directory and immediately understand the URL structure without following nested folder hierarchies. This trades the visual nesting of folders for the clarity of seeing all routes at once, which is particularly valuable in larger applications where routes number in the dozens.

Pathless layout routes (underscore prefix) and index routes (_index.tsx) are mechanisms for expressing UI structure that does not map one-to-one with URL structure. A group of auth pages might share a centered card layout without introducing an /auth/ URL segment. An index route provides default content when the parent route has no active child. These features let you design the UI hierarchy independently from the URL hierarchy.

Anti-Patterns

  • Forgetting <Outlet /> in a parent route. Without the outlet, child routes have no place to render. The parent layout appears but the child content is silently invisible, which is a common source of confusion during development.

  • Hard-coding parent paths in nested route links. Using <Link to="/concerts/trending"> inside a concert route couples the component to its exact URL position. Use relative links (<Link to="trending">) so the component works regardless of where it is mounted in the route tree.

  • Putting business logic in layout routes. Layout routes should handle shared chrome (navigation, headers, footers) and render the outlet. Data fetching for specific content belongs in the child routes that display that content.

  • Creating both dashboard.tsx and dashboard/_index.tsx without understanding their relationship. The layout file (dashboard.tsx) wraps all dashboard children, and _index.tsx is the default child. If the layout file has no <Outlet />, the index page never renders. If both files try to fetch the same data, the work is duplicated.

  • Overusing splat routes as catch-alls. A $.tsx file matches everything, which makes it tempting to use as a universal router. But it bypasses Remix's type-safe params, loses the benefits of nested routing, and makes the route tree harder to understand.

Best Practices

  • Keep layout routes thin — they should handle shared chrome (nav, sidebar) and render <Outlet />, not business logic.
  • Use _index.tsx for every parent route so there is always a default child to render.
  • Prefer relative <Link to="..."> inside nested routes to avoid hard-coding parent paths.
  • Use useMatches() to access data from any active route in the hierarchy without prop drilling.
  • Use the handle export on routes to pass metadata (breadcrumbs, titles) up the route tree.

Common Pitfalls

  • Forgetting <Outlet /> in a parent route — the child content will never appear.
  • Confusing . (nesting separator) with / (not valid in flat route filenames).
  • Using $ in a route filename but forgetting the param name when accessing useParams().
  • Placing a file at app/routes/dashboard.tsx and app/routes/dashboard._index.tsx but omitting the <Outlet /> in the dashboard file, so the index child never renders.
  • Not returning a response from a loader in a layout route — even if the layout has no data needs, omitting the loader is fine, but returning undefined from an existing loader is not.

Install this skill directly: skilldb add remix-skills

Get CLI access →