UNPKG

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