| title | Route File Naming |
|---|
The route file convention is changing in v2. You can prepare for this change at your convenience with the v2_routeConvention future flag. For instructions on making this change see the v2 guide.
Setting up routes in Remix is as simple as creating files in your app directory. These are the conventions you should know to understand how routing in Remix works.
Please note that you can use either .js, .jsx, .ts or .tsx file extensions. We'll stick with .tsx in the examples to avoid duplication.
app/
├── routes/
└── root.tsxThe file in app/root.tsx is your root layout, or "root route" (very sorry for those of you who pronounce those words the same way!). It works just like all other routes:
- You can export a
loader,action,meta,headers, orlinksfunction - You can export an
ErrorBoundaryorCatchBoundary - Your default export is the layout component that renders the rest of your app in an
<Outlet />
Any JavaScript or TypeScript files in the app/routes/ directory will become routes in your application. The filename maps to the route's URL pathname, except for index.tsx which maps to the root pathname.
app/
├── routes/
│ ├── about.tsx
│ └── index.tsx
└── root.tsx| URL | Matched Route |
|---|---|
/ |
app/routes/index.tsx |
/about |
app/routes/about.tsx |
The default export in this file is the component that is rendered at that route and will render within the <Outlet /> rendered by the root route.
app/
├── routes/
│ ├── blog/
│ │ ├── $postId.tsx
│ │ ├── categories.tsx
│ │ └── index.tsx
│ ├── about.tsx
│ └── index.tsx
└── root.tsxURL Route Matches
| URL | Matched Route |
|---|---|
/blog |
app/routes/blog/index.tsx |
/blog/categories |
app/routes/blog/categories.tsx |
/blog/my-post |
app/routes/blog/$postId.tsx |
Routes that begin with a $ character indicate the name of a dynamic segment of the URL. It will be parsed and passed to your loader and action data as a value on the param object.
For example: app/routes/blog/$postId.tsx will match the following URLs:
/blog/my-story/blog/once-upon-a-time/blog/how-to-ride-a-bike
On each of these pages, the dynamic segment of the URL path is the value of the parameter. There can be multiple parameters active at any time (as in /dashboard/:client/invoices/:invoiceId view example app) and all parameters can be accessed within components via useParams and within loaders/actions via the argument's params property:
import type {
ActionArgs,
LoaderArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { useParams } from "@remix-run/react";
export const loader = async ({ params }: LoaderArgs) => {
console.log(params.postId);
};
export const action = async ({ params }: ActionArgs) => {
console.log(params.postId);
};
export default function PostRoute() {
const params = useParams();
console.log(params.postId);
}Nested routes can also contain dynamic segments by using the $ character in the parent's directory name. For example, app/routes/blog/$postId/edit.tsx might represent the editor page for blog entries.
See the routing guide for more information.
Wrapping a route segment in parens will make the segment optional.
app/
├── routes/
│ ├── ($lang)/
│ │ ├── $pid.tsx
│ │ ├── categories.tsx
│ └── index.tsx
└── root.tsxURL Route Matches
| URL | Matched Route |
|---|---|
/categories |
app/routes/($lang)/categories.tsx |
/en/categories |
app/routes/($lang)/categories.tsx |
/fr/categories |
app/routes/($lang)/categories.tsx |
/american-flag-speedo |
app/routes/($lang)/$pid.tsx |
/en/american-flag-speedo |
app/routes/($lang)/$pid.tsx |
/fr/american-flag-speedo |
app/routes/($lang)/$pid.tsx |
app/
├── routes/
│ ├── blog/
│ │ ├── $postId.tsx
│ │ ├── categories.tsx
│ │ └── index.tsx
│ ├── about.tsx
│ ├── blog.tsx
│ └── index.tsx
└── root.tsxURL Route Matches
| URL | Matched Route | Layout |
|---|---|---|
/ |
app/routes/index.tsx |
app/root.tsx |
/about |
app/routes/about.tsx |
app/root.tsx |
/blog |
app/routes/blog/index.tsx |
app/routes/blog.tsx |
/blog/categories |
app/routes/blog/categories.tsx |
app/routes/blog.tsx |
/blog/my-post |
app/routes/blog/$postId.tsx |
app/routes/blog.tsx |
In the example above, the blog.tsx is a "layout route" for everything within the blog directory (blog/index.tsx and blog/categories.tsx). When a route has the same name as its directory (routes/blog.tsx and routes/blog/), it becomes a layout route for all the routes inside that directory ("child routes"). Similar to your root route, the parent route should render an <Outlet /> where the child routes should appear. This is how you can create multiple levels of persistent layout nesting associated with URLs.
app/
├── routes/
│ ├── __app/
│ │ ├── dashboard.tsx
│ │ └── $userId/
│ │ └── profile.tsx
│ └── __marketing
│ │ ├── index.tsx
│ │ └── product.tsx
│ ├── __app.tsx
│ └── __marketing.tsx
└── root.tsxURL Route Matches
| URL | Matched Route | Layout |
|---|---|---|
/ |
app/routes/__marketing/index.tsx |
app/routes/__marketing.tsx |
/product |
app/routes/__marketing/product.tsx |
app/routes/__marketing.tsx |
/dashboard |
app/routes/__app/dashboard.tsx |
app/routes/__app.tsx |
/chance/profile |
app/routes/__app/$userId/profile.tsx |
app/routes/__app.tsx |
You can also create layout routes without adding segments to the URL by prepending the directory and associated parent route file with double underscores: __.
For example, all of your marketing pages could be in app/routes/__marketing/* and then share a layout by creating app/routes/__marketing.tsx. A route app/routes/__marketing/product.tsx would be accessible at the /product URL because __marketing won't add segments to the URL, just UI hierarchy.
Be careful, pathless layout routes introduce the possibility of URL conflicts
app/
├── routes/
│ ├── blog/
│ │ ├── $postId.tsx
│ │ ├── categories.tsx
│ │ └── index.tsx
│ ├── about.tsx
│ ├── blog.authors.tsx
│ ├── blog.tsx
│ └── index.tsx
└── root.tsxURL Route Matches
| URL | Matched Route | Layout |
|---|---|---|
/blog |
app/routes/blog/index.tsx |
app/routes/blog.tsx |
/blog/categories |
app/routes/blog/categories.tsx |
app/routes/blog.tsx |
/blog/authors |
app/routes/blog.authors.tsx |
app/root.tsx |
By creating a file with . characters between segments, you can create a nested URL without nested layouts. For example, a file app/routes/blog.authors.tsx will route to the pathname /blog/authors, but it will not share a layout with routes in the app/routes/blog/ directory.
app/
├── routes/
│ ├── blog/
│ │ ├── $postId.tsx
│ │ ├── categories.tsx
│ │ └── index.tsx
│ ├── $.tsx
│ ├── about.tsx
│ ├── blog.authors.tsx
│ ├── blog.tsx
│ └── index.tsx
└── root.tsxURL Route Matches
| URL | Matched Route | Layout |
|---|---|---|
/ |
app/routes/index.tsx |
app/root.tsx |
/blog |
app/routes/blog/index.tsx |
app/routes/blog.tsx |
/somewhere-else |
app/routes/$.tsx |
app/root.tsx |
Files that are named $.tsx are called "splat" (or "catch-all") routes. These routes will map to any URL not matched by other route files in the same directory.
Similar to dynamic route parameters, you can access the value of the matched path on the splat route's params with the "*" key.
import type {
ActionArgs,
LoaderArgs,
} from "@remix-run/node"; // or cloudflare/deno
import { useParams } from "@remix-run/react";
export const loader = async ({ params }: LoaderArgs) => {
console.log(params["*"]);
};
export const action = async ({ params }: ActionArgs) => {
console.log(params["*"]);
};
export default function PostRoute() {
const params = useParams();
console.log(params["*"]);
}Because some characters have special meaning, you must use our escaping syntax if you want those characters to actually appear in the route. For example, if I wanted to make a Resource Route for a /sitemap.xml, I could name the file app/routes/[sitemap.xml].tsx. So you simply wrap any part of the filename with brackets and that will escape any special characters.