Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 20f9592

Browse files
authoredSep 13, 2024··
Add file route conventions guide (#11992)
1 parent 05e4905 commit 20f9592

File tree

2 files changed

+381
-2
lines changed

2 files changed

+381
-2
lines changed
 

‎docs/guides/file-route-conventions.md

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

‎docs/start/routing.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export const routes: RouteConfig = [
3636

3737
## File System Routes
3838

39-
If you prefer a file system routing convention, the `@react-router/fs-routes` package provides support for [Remix-style flat routes.][route_file_naming]
39+
If you prefer to define your routes via file naming conventions rather than configuration, the `@react-router/fs-routes` package provides a [file system routing convention.][file-route-conventions]
4040

4141
```tsx filename=app/routes.ts
4242
import { type RouteConfig } from "@react-router/dev/routes";
@@ -279,6 +279,6 @@ function Wizard() {
279279

280280
Note that these routes do not participate in data loading, actions, code splitting, or any other route module features, so their use cases are more limited than those of the route module.
281281

282-
[route_file_naming]: ../file-conventions/routes
282+
[file-route-conventions]: ../guides/file-route-conventions
283283
[outlet]: ../components/outlet
284284
[code_splitting]: ../discussion/code-splitting

0 commit comments

Comments
 (0)
Please sign in to comment.