Routing
Nested routes, dynamic segments, outlets, and file-based routing conventions in Remix
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 linesRouting — 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.tsxanddashboard/_index.tsxwithout understanding their relationship. The layout file (dashboard.tsx) wraps all dashboard children, and_index.tsxis 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
$.tsxfile 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.tsxfor 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
handleexport 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 accessinguseParams(). - Placing a file at
app/routes/dashboard.tsxandapp/routes/dashboard._index.tsxbut 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
undefinedfrom an existing loader is not.
Install this skill directly: skilldb add remix-skills
Related Skills
Actions
Server-side form mutations with action functions, progressive enhancement, and optimistic UI in Remix
Authentication
Authentication patterns in Remix using sessions, cookies, JWT, OAuth flows, and route protection
Deployment
Deploying Remix applications to various platforms including Vercel, Fly.io, Cloudflare, and Node.js servers
Error Handling
Error boundaries, catch boundaries, and structured error handling strategies in Remix applications
Loaders
Server-side data loading with loader functions, useLoaderData, and type-safe data fetching in Remix
Streaming
Streaming responses and deferred data loading with defer, Await, and Suspense in Remix