UNPKG

16.6 kB Markdown View Raw
1---
2title: File Route Conventions
3---
4
5# File Route Conventions
6
7[MODES: framework]
8
9<br/>
10<br/>
11
12The `@react-router/fs-routes` package enables file-convention based route config.
13
14## Setting up
15
16First install the `@react-router/fs-routes` package:
17
18```shellscript nonumber
19npm i @react-router/fs-routes
20```
21
22Then use it to provide route config in your `app/routes.ts` file:
23
24```tsx filename=app/routes.ts
25import { type RouteConfig } from "@react-router/dev/routes";
26import { flatRoutes } from "@react-router/fs-routes";
27
28export default flatRoutes() satisfies RouteConfig;
29```
30
31Any modules in the `app/routes` directory will become routes in your application by default.
32The `ignoredRouteFiles` option allows you to specify files that should not be included as routes:
33
34```tsx filename=app/routes.ts
35import { type RouteConfig } from "@react-router/dev/routes";
36import { flatRoutes } from "@react-router/fs-routes";
37
38export default flatRoutes({
39 ignoredRouteFiles: ["home.tsx"],
40}) satisfies RouteConfig;
41```
42
43This will look for routes in the `app/routes` directory by default, but this can be configured via the `rootDirectory` option which is relative to your app directory:
44
45```tsx filename=app/routes.ts
46import { type RouteConfig } from "@react-router/dev/routes";
47import { flatRoutes } from "@react-router/fs-routes";
48
49export default flatRoutes({
50 rootDirectory: "file-routes",
51}) satisfies RouteConfig;
52```
53
54The rest of this guide will assume you're using the default `app/routes` directory.
55
56## Basic Routes
57
58The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index_route] for the [root route][root_route]. You can use `.js`, `.jsx`, `.ts` or `.tsx` file extensions.
59
60```text lines=[3-4]
61app/
62├── routes/
63│ ├── _index.tsx
64│ └── about.tsx
65└── root.tsx
66```
67
68| URL | Matched Routes |
69| -------- | ----------------------- |
70| `/` | `app/routes/_index.tsx` |
71| `/about` | `app/routes/about.tsx` |
72
73Note that these routes will be rendered in the outlet of `app/root.tsx` because of [nested routing][nested_routing].
74
75## Dot Delimiters
76
77Adding a `.` to a route filename will create a `/` in the URL.
78
79```text lines=[5-7]
80 app/
81├── routes/
82│ ├── _index.tsx
83│ ├── about.tsx
84│ ├── concerts.trending.tsx
85│ ├── concerts.salt-lake-city.tsx
86│ └── concerts.san-diego.tsx
87└── root.tsx
88```
89
90| URL | Matched Route |
91| -------------------------- | ---------------------------------------- |
92| `/` | `app/routes/_index.tsx` |
93| `/about` | `app/routes/about.tsx` |
94| `/concerts/trending` | `app/routes/concerts.trending.tsx` |
95| `/concerts/salt-lake-city` | `app/routes/concerts.salt-lake-city.tsx` |
96| `/concerts/san-diego` | `app/routes/concerts.san-diego.tsx` |
97
98The dot delimiter also creates nesting, see the [nesting section][nested_routes] for more information.
99
100## Dynamic Segments
101
102Usually your URLs aren't static but data-driven. Dynamic segments allow you to match segments of the URL and use that value in your code. You create them with the `$` prefix.
103
104```text lines=[5]
105 app/
106├── routes/
107│ ├── _index.tsx
108│ ├── about.tsx
109│ ├── concerts.$city.tsx
110│ └── concerts.trending.tsx
111└── root.tsx
112```
113
114| URL | Matched Route |
115| -------------------------- | ---------------------------------- |
116| `/` | `app/routes/_index.tsx` |
117| `/about` | `app/routes/about.tsx` |
118| `/concerts/trending` | `app/routes/concerts.trending.tsx` |
119| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` |
120| `/concerts/san-diego` | `app/routes/concerts.$city.tsx` |
121
122The value will be parsed from the URL and passed to various APIs. We call these values "URL Parameters". The most useful places to access the URL params are in [loaders] and [actions].
123
124```tsx
125export async function loader({ params }) {
126 return fakeDb.getAllConcertsForCity(params.city);
127}
128```
129
130You'll note the property name on the `params` object maps directly to the name of your file: `$city.tsx` becomes `params.city`.
131
132Routes can have multiple dynamic segments, like `concerts.$city.$date`, both are accessed on the params object by name:
133
134```tsx
135export async function loader({ params }) {
136 return fake.db.getConcerts({
137 date: params.date,
138 city: params.city,
139 });
140}
141```
142
143See the [routing guide][routing_guide] for more information.
144
145## Nested Routes
146
147Nested Routing is the general idea of coupling segments of the URL to component hierarchy and data. You can read more about it in the [Routing Guide][nested_routing].
148
149You create nested routes with [dot delimiters][dot_delimiters]. If the filename before the `.` matches another route filename, it automatically becomes a child route to the matching parent. Consider these routes:
150
151```text lines=[5-8]
152 app/
153├── routes/
154│ ├── _index.tsx
155│ ├── about.tsx
156│ ├── concerts._index.tsx
157│ ├── concerts.$city.tsx
158│ ├── concerts.trending.tsx
159│ └── concerts.tsx
160└── root.tsx
161```
162
163All the routes that start with `app/routes/concerts.` will be child routes of `app/routes/concerts.tsx` and render inside the [parent route's outlet][nested_routing].
164
165| URL | Matched Route | Layout |
166| -------------------------- | ---------------------------------- | ------------------------- |
167| `/` | `app/routes/_index.tsx` | `app/root.tsx` |
168| `/about` | `app/routes/about.tsx` | `app/root.tsx` |
169| `/concerts` | `app/routes/concerts._index.tsx` | `app/routes/concerts.tsx` |
170| `/concerts/trending` | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` |
171| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` |
172
173Note you typically want to add an index route when you add nested routes so that something renders inside the parent's outlet when users visit the parent URL directly.
174
175For example, if the URL is `/concerts/salt-lake-city` then the UI hierarchy will look like this:
176
177```tsx
178<Root>
179 <Concerts>
180 <City />
181 </Concerts>
182</Root>
183```
184
185## Nested URLs without Layout Nesting
186
187Sometimes you want the URL to be nested, but you don't want the automatic layout nesting. You can opt out of nesting with a trailing underscore on the parent segment:
188
189```text lines=[8]
190 app/
191├── routes/
192│ ├── _index.tsx
193│ ├── about.tsx
194│ ├── concerts.$city.tsx
195│ ├── concerts.trending.tsx
196│ ├── concerts.tsx
197│ └── concerts_.mine.tsx
198└── root.tsx
199```
200
201| URL | Matched Route | Layout |
202| -------------------------- | ---------------------------------- | ------------------------- |
203| `/` | `app/routes/_index.tsx` | `app/root.tsx` |
204| `/about` | `app/routes/about.tsx` | `app/root.tsx` |
205| `/concerts/mine` | `app/routes/concerts_.mine.tsx` | `app/root.tsx` |
206| `/concerts/trending` | `app/routes/concerts.trending.tsx` | `app/routes/concerts.tsx` |
207| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` |
208
209Note that `/concerts/mine` does not nest with `app/routes/concerts.tsx` anymore, but `app/root.tsx`. The `trailing_` underscore creates a path segment, but it does not create layout nesting.
210
211Think of the `trailing_` underscore as the long bit at the end of your parent's signature, writing you out of the will, removing the segment that follows from the layout nesting.
212
213## Nested Layouts without Nested URLs
214
215We call these <a name="pathless-routes"><b>Pathless Routes</b></a>
216
217Sometimes you want to share a layout with a group of routes without adding any path segments to the URL. A common example is a set of authentication routes that have a different header/footer than the public pages or the logged in app experience. You can do this with a `_leading` underscore.
218
219```text lines=[3-5]
220 app/
221├── routes/
222│ ├── _auth.login.tsx
223│ ├── _auth.register.tsx
224│ ├── _auth.tsx
225│ ├── _index.tsx
226│ ├── concerts.$city.tsx
227│ └── concerts.tsx
228└── root.tsx
229```
230
231| URL | Matched Route | Layout |
232| -------------------------- | ------------------------------- | ------------------------- |
233| `/` | `app/routes/_index.tsx` | `app/root.tsx` |
234| `/login` | `app/routes/_auth.login.tsx` | `app/routes/_auth.tsx` |
235| `/register` | `app/routes/_auth.register.tsx` | `app/routes/_auth.tsx` |
236| `/concerts` | `app/routes/concerts.tsx` | `app/routes/concerts.tsx` |
237| `/concerts/salt-lake-city` | `app/routes/concerts.$city.tsx` | `app/routes/concerts.tsx` |
238
239Think of the `_leading` underscore as a blanket you're pulling over the filename, hiding the filename from the URL.
240
241## Optional Segments
242
243Wrapping a route segment in parentheses will make the segment optional.
244
245```text lines=[3-5]
246 app/
247├── routes/
248│ ├── ($lang)._index.tsx
249│ ├── ($lang).$productId.tsx
250│ └── ($lang).categories.tsx
251└── root.tsx
252```
253
254| URL | Matched Route |
255| -------------------------- | ----------------------------------- |
256| `/` | `app/routes/($lang)._index.tsx` |
257| `/categories` | `app/routes/($lang).categories.tsx` |
258| `/en/categories` | `app/routes/($lang).categories.tsx` |
259| `/fr/categories` | `app/routes/($lang).categories.tsx` |
260| `/american-flag-speedo` | `app/routes/($lang)._index.tsx` |
261| `/en/american-flag-speedo` | `app/routes/($lang).$productId.tsx` |
262| `/fr/american-flag-speedo` | `app/routes/($lang).$productId.tsx` |
263
264You may wonder why `/american-flag-speedo` is matching the `($lang)._index.tsx` route instead of `($lang).$productId.tsx`. This is because when you have an optional dynamic param segment followed by another dynamic param, it cannot reliably be determined if a single-segment URL such as `/american-flag-speedo` should match `/:lang` `/:productId`. Optional segments match eagerly and thus it will match `/:lang`. If you have this type of setup it's recommended to look at `params.lang` in the `($lang)._index.tsx` loader and redirect to `/:lang/american-flag-speedo` for the current/default language if `params.lang` is not a valid language code.
265
266## Splat Routes
267
268While [dynamic segments][dynamic_segments] match a single path segment (the stuff between two `/` in a URL), a splat route will match the rest of a URL, including the slashes.
269
270```text lines=[4,6]
271 app/
272├── routes/
273│ ├── _index.tsx
274│ ├── $.tsx
275│ ├── about.tsx
276│ └── files.$.tsx
277└── root.tsx
278```
279
280| URL | Matched Route |
281| -------------------------------------------- | ------------------------ |
282| `/` | `app/routes/_index.tsx` |
283| `/about` | `app/routes/about.tsx` |
284| `/beef/and/cheese` | `app/routes/$.tsx` |
285| `/files` | `app/routes/files.$.tsx` |
286| `/files/talks/react-conf_old.pdf` | `app/routes/files.$.tsx` |
287| `/files/talks/react-conf_final.pdf` | `app/routes/files.$.tsx` |
288| `/files/talks/react-conf-FINAL-MAY_2024.pdf` | `app/routes/files.$.tsx` |
289
290Similar to dynamic route parameters, you can access the value of the matched path on the splat route's `params` with the `"*"` key.
291
292```tsx filename=app/routes/files.$.tsx
293export async function loader({ params }) {
294 const filePath = params["*"];
295 return fake.getFileInfo(filePath);
296}
297```
298
299## Catch-all Route
300
301To create a route that will match any requests that don't match other defined routes (such as a 404 page), create a file named `$.tsx` within your routes directory:
302
303| URL | Matched Route |
304| ------------------------------ | ----------------------- |
305| `/` | `app/routes/_index.tsx` |
306| `/about` | `app/routes/about.tsx` |
307| `/any-invalid-path-will-match` | `app/routes/$.tsx` |
308
309By default the matched route will return a 200 response, so be sure to modify your catchall route to return a 404 instead:
310
311```tsx filename=app/routes/$.tsx
312export async function loader() {
313 return data({}, 404);
314}
315```
316
317## Escaping Special Characters
318
319If you want one of the special characters used for these route conventions to actually be a part of the URL, you can escape the conventions with `[]` characters. This can be especially helpful for [resource routes][resource_routes] that include an extension in the URL.
320
321| Filename | URL |
322| ----------------------------------- | ------------------- |
323| `app/routes/sitemap[.]xml.tsx` | `/sitemap.xml` |
324| `app/routes/[sitemap.xml].tsx` | `/sitemap.xml` |
325| `app/routes/weird-url.[_index].tsx` | `/weird-url/_index` |
326| `app/routes/dolla-bills-[$].tsx` | `/dolla-bills-$` |
327| `app/routes/[[so-weird]].tsx` | `/[so-weird]` |
328| `app/routes/reports.$id[.pdf].ts` | `/reports/123.pdf` |
329
330## Folders for Organization
331
332Routes can also be folders with a `route.tsx` file inside defining the route module. The rest of the files in the folder will not become routes. This allows you to organize your code closer to the routes that use them instead of repeating the feature names across other folders.
333
334The files inside a folder have no meaning for the route paths, the route path is completely defined by the folder name.
335
336Consider these routes:
337
338```text
339 app/
340├── routes/
341│ ├── _landing._index.tsx
342│ ├── _landing.about.tsx
343│ ├── _landing.tsx
344│ ├── app._index.tsx
345│ ├── app.projects.tsx
346│ ├── app.tsx
347│ └── app_.projects.$id.roadmap.tsx
348└── root.tsx
349```
350
351Some, or all of them can be folders holding their own `route` module inside.
352
353```text
354app/
355├── routes/
356│ ├── _landing._index/
357│ │ ├── route.tsx
358│ │ └── scroll-experience.tsx
359│ ├── _landing.about/
360│ │ ├── employee-profile-card.tsx
361│ │ ├── get-employee-data.server.ts
362│ │ ├── route.tsx
363│ │ └── team-photo.jpg
364│ ├── _landing/
365│ │ ├── footer.tsx
366│ │ ├── header.tsx
367│ │ └── route.tsx
368│ ├── app._index/
369│ │ ├── route.tsx
370│ │ └── stats.tsx
371│ ├── app.projects/
372│ │ ├── get-projects.server.ts
373│ │ ├── project-buttons.tsx
374│ │ ├── project-card.tsx
375│ │ └── route.tsx
376│ ├── app/
377│ │ ├── footer.tsx
378│ │ ├── primary-nav.tsx
379│ │ └── route.tsx
380│ ├── app_.projects.$id.roadmap/
381│ │ ├── chart.tsx
382│ │ ├── route.tsx
383│ │ └── update-timeline.server.ts
384│ └── contact-us.tsx
385└── root.tsx
386```
387
388Note that when you turn a route module into a folder, the route module becomes `folder/route.tsx`, all other modules in the folder will not become routes. For example:
389
390```
391# these are the same route:
392app/routes/app.tsx
393app/routes/app/route.tsx
394
395# as are these
396app/routes/app._index.tsx
397app/routes/app._index/route.tsx
398```
399
400[route-config-file]: ../start/framework/routing#configuring-routes
401[loaders]: ../start/framework/data-loading
402[actions]: ../start/framework/actions
403[routing_guide]: ../start/framework/routing
404[root_route]: ../start/framework/route-module
405[index_route]: ../start/framework/routing#index-routes
406[nested_routing]: ../start/framework/routing#nested-routes
407[nested_routes]: #nested-routes
408[dot_delimiters]: #dot-delimiters
409[dynamic_segments]: #dynamic-segments
410[resource_routes]: ../how-to/resource-routes