-
-
Notifications
You must be signed in to change notification settings - Fork 10.6k
/
Copy pathllm.txt
15107 lines (11106 loc) · 411 KB
/
llm.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
title: Server vs. Client Code Execution
hidden: true
---
---
title: Sessions and Cookies
---
# Sessions and Cookies
## Sessions
Sessions are an important part of websites that allow the server to identify requests coming from the same person, especially when it comes to server-side form validation or when JavaScript is not on the page. Sessions are a fundamental building block of many sites that let users "log in", including social, e-commerce, business, and educational websites.
When using React Router as your framework, sessions are managed on a per-route basis (rather than something like express middleware) in your `loader` and `action` methods using a "session storage" object (that implements the [`SessionStorage`][session-storage] interface). Session storage understands how to parse and generate cookies, and how to store session data in a database or filesystem.
### Using Sessions
This is an example of a cookie session storage:
```ts filename=app/sessions.server.ts
import { createCookieSessionStorage } from "react-router";
type SessionData = {
userId: string;
};
type SessionFlashData = {
error: string;
};
const { getSession, commitSession, destroySession } =
createCookieSessionStorage<SessionData, SessionFlashData>(
{
// a Cookie from `createCookie` or the CookieOptions to create one
cookie: {
name: "__session",
// all of these are optional
domain: "reactrouter.com",
// Expires can also be set (although maxAge overrides it when used in combination).
// Note that this method is NOT recommended as `new Date` creates only one date on each server deployment, not a dynamic date in the future!
//
// expires: new Date(Date.now() + 60_000),
httpOnly: true,
maxAge: 60,
path: "/",
sameSite: "lax",
secrets: ["s3cret1"],
secure: true,
},
}
);
export { getSession, commitSession, destroySession };
```
We recommend setting up your session storage object in `app/sessions.server.ts` so all routes that need to access session data can import from the same spot.
The input/output to a session storage object are HTTP cookies. `getSession()` retrieves the current session from the incoming request's `Cookie` header, and `commitSession()`/`destroySession()` provide the `Set-Cookie` header for the outgoing response.
You'll use methods to get access to sessions in your `loader` and `action` functions.
After retrieving a session with `getSession`, the returned session object has a handful of methods and properties:
```tsx
export async function action({
request,
}: ActionFunctionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
session.get("foo");
session.has("bar");
// etc.
}
```
See the [Session API][session-api] for more all the methods available on the session object.
### Login form example
A login form might look something like this:
```tsx filename=app/routes/login.tsx lines=[4-7,12-14,16,22,25,33-35,46,51,56,61]
import { data, redirect } from "react-router";
import type { Route } from "./+types/login";
import {
getSession,
commitSession,
} from "../sessions.server";
export async function loader({
request,
}: Route.LoaderArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
if (session.has("userId")) {
// Redirect to the home page if they are already signed in.
return redirect("/");
}
return data(
{ error: session.get("error") },
{
headers: {
"Set-Cookie": await commitSession(session),
},
}
);
}
export async function action({
request,
}: Route.ActionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
const form = await request.formData();
const username = form.get("username");
const password = form.get("password");
const userId = await validateCredentials(
username,
password
);
if (userId == null) {
session.flash("error", "Invalid username/password");
// Redirect back to the login page with errors.
return redirect("/login", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
session.set("userId", userId);
// Login succeeded, send them to the home page.
return redirect("/", {
headers: {
"Set-Cookie": await commitSession(session),
},
});
}
export default function Login({
loaderData,
}: Route.ComponentProps) {
const { error } = loaderData;
return (
<div>
{error ? <div className="error">{error}</div> : null}
<form method="POST">
<div>
<p>Please sign in</p>
</div>
<label>
Username: <input type="text" name="username" />
</label>
<label>
Password:{" "}
<input type="password" name="password" />
</label>
</form>
</div>
);
}
```
And then a logout form might look something like this:
```tsx filename=app/routes/logout.tsx
import {
getSession,
destroySession,
} from "../sessions.server";
import type { Route } from "./+types/logout";
export async function action({
request,
}: Route.ActionArgs) {
const session = await getSession(
request.headers.get("Cookie")
);
return redirect("/login", {
headers: {
"Set-Cookie": await destroySession(session),
},
});
}
export default function LogoutRoute() {
return (
<>
<p>Are you sure you want to log out?</p>
<Form method="post">
<button>Logout</button>
</Form>
<Link to="/">Never mind</Link>
</>
);
}
```
<docs-warning>It's important that you logout (or perform any mutation for that matter) in an `action` and not a `loader`. Otherwise you open your users to [Cross-Site Request Forgery][csrf] attacks.</docs-warning>
### Session Gotchas
Because of nested routes, multiple loaders can be called to construct a single page. When using `session.flash()` or `session.unset()`, you need to be sure no other loaders in the request are going to want to read that, otherwise you'll get race conditions. Typically if you're using flash, you'll want to have a single loader read it, if another loader wants a flash message, use a different key for that loader.
### Creating custom session storage
React Router makes it easy to store sessions in your own database if needed. The [`createSessionStorage()`][create-session-storage] API requires a `cookie` (for options for creating a cookie, see [cookies][cookies]) and a set of create, read, update, and delete (CRUD) methods for managing the session data. The cookie is used to persist the session ID.
- `createData` will be called from `commitSession` on the initial session creation when no session ID exists in the cookie
- `readData` will be called from `getSession` when a session ID exists in the cookie
- `updateData` will be called from `commitSession` when a session ID already exists in the cookie
- `deleteData` is called from `destroySession`
The following example shows how you could do this using a generic database client:
```ts
import { createSessionStorage } from "react-router";
function createDatabaseSessionStorage({
cookie,
host,
port,
}) {
// Configure your database client...
const db = createDatabaseClient(host, port);
return createSessionStorage({
cookie,
async createData(data, expires) {
// `expires` is a Date after which the data should be considered
// invalid. You could use it to invalidate the data somehow or
// automatically purge this record from your database.
const id = await db.insert(data);
return id;
},
async readData(id) {
return (await db.select(id)) || null;
},
async updateData(id, data, expires) {
await db.update(id, data);
},
async deleteData(id) {
await db.delete(id);
},
});
}
```
And then you can use it like this:
```ts
const { getSession, commitSession, destroySession } =
createDatabaseSessionStorage({
host: "localhost",
port: 1234,
cookie: {
name: "__session",
sameSite: "lax",
},
});
```
The `expires` argument to `createData` and `updateData` is the same `Date` at which the cookie itself expires and is no longer valid. You can use this information to automatically purge the session record from your database to save on space, or to ensure that you do not otherwise return any data for old, expired cookies.
### Additional session utils
There are also several other session utilities available if you need them:
- [`isSession`][is-session]
- [`createMemorySessionStorage`][create-memory-session-storage]
- [`createSession`][create-session] (custom storage)
- [`createFileSessionStorage`][create-file-session-storage] (node)
- [`createWorkersKVSessionStorage`][create-workers-kv-session-storage] (Cloudflare Workers)
- [`createArcTableSessionStorage`][create-arc-table-session-storage] (architect, Amazon DynamoDB)
## Cookies
A [cookie][cookie] is a small piece of information that your server sends someone in a HTTP response that their browser will send back on subsequent requests. This technique is a fundamental building block of many interactive websites that adds state so you can build authentication (see [sessions][sessions]), shopping carts, user preferences, and many other features that require remembering who is "logged in".
React Router's [`Cookie` interface][cookie-api] provides a logical, reusable container for cookie metadata.
### Using cookies
While you may create these cookies manually, it is more common to use a [session storage][sessions].
In React Router, you will typically work with cookies in your `loader` and/or `action` functions, since those are the places where you need to read and write data.
Let's say you have a banner on your e-commerce site that prompts users to check out the items you currently have on sale. The banner spans the top of your homepage, and includes a button on the side that allows the user to dismiss the banner so they don't see it for at least another week.
First, create a cookie:
```ts filename=app/cookies.server.ts
import { createCookie } from "react-router";
export const userPrefs = createCookie("user-prefs", {
maxAge: 604_800, // one week
});
```
Then, you can `import` the cookie and use it in your `loader` and/or `action`. The `loader` in this case just checks the value of the user preference so you can use it in your component for deciding whether to render the banner. When the button is clicked, the `<form>` calls the `action` on the server and reloads the page without the banner.
### User preferences example
```tsx filename=app/routes/home.tsx lines=[4,9-11,18-20,29]
import { Link, Form, redirect } from "react-router";
import type { Route } from "./+types/home";
import { userPrefs } from "../cookies.server";
export async function loader({
request,
}: Route.LoaderArgs) {
const cookieHeader = request.headers.get("Cookie");
const cookie =
(await userPrefs.parse(cookieHeader)) || {};
return { showBanner: cookie.showBanner };
}
export async function action({
request,
}: Route.ActionArgs) {
const cookieHeader = request.headers.get("Cookie");
const cookie =
(await userPrefs.parse(cookieHeader)) || {};
const bodyParams = await request.formData();
if (bodyParams.get("bannerVisibility") === "hidden") {
cookie.showBanner = false;
}
return redirect("/", {
headers: {
"Set-Cookie": await userPrefs.serialize(cookie),
},
});
}
export default function Home({
loaderData,
}: Route.ComponentProps) {
return (
<div>
{loaderData.showBanner ? (
<div>
<Link to="/sale">Don't miss our sale!</Link>
<Form method="post">
<input
type="hidden"
name="bannerVisibility"
value="hidden"
/>
<button type="submit">Hide</button>
</Form>
</div>
) : null}
<h1>Welcome!</h1>
</div>
);
}
```
### Cookie attributes
Cookies have [several attributes][cookie-attrs] that control when they expire, how they are accessed, and where they are sent. Any of these attributes may be specified either in `createCookie(name, options)`, or during `serialize()` when the `Set-Cookie` header is generated.
```ts
const cookie = createCookie("user-prefs", {
// These are defaults for this cookie.
path: "/",
sameSite: "lax",
httpOnly: true,
secure: true,
expires: new Date(Date.now() + 60_000),
maxAge: 60,
});
// You can either use the defaults:
cookie.serialize(userPrefs);
// Or override individual ones as needed:
cookie.serialize(userPrefs, { sameSite: "strict" });
```
Please read [more info about these attributes][cookie-attrs] to get a better understanding of what they do.
### Signing cookies
It is possible to sign a cookie to automatically verify its contents when it is received. Since it's relatively easy to spoof HTTP headers, this is a good idea for any information that you do not want someone to be able to fake, like authentication information (see [sessions][sessions]).
To sign a cookie, provide one or more `secrets` when you first create the cookie:
```ts
const cookie = createCookie("user-prefs", {
secrets: ["s3cret1"],
});
```
Cookies that have one or more `secrets` will be stored and verified in a way that ensures the cookie's integrity.
Secrets may be rotated by adding new secrets to the front of the `secrets` array. Cookies that have been signed with old secrets will still be decoded successfully in `cookie.parse()`, and the newest secret (the first one in the array) will always be used to sign outgoing cookies created in `cookie.serialize()`.
```ts filename=app/cookies.server.ts
export const cookie = createCookie("user-prefs", {
secrets: ["n3wsecr3t", "olds3cret"],
});
```
```tsx filename=app/routes/my-route.tsx
import { data } from "react-router";
import { cookie } from "../cookies.server";
import type { Route } from "./+types/my-route";
export async function loader({
request,
}: Route.LoaderArgs) {
const oldCookie = request.headers.get("Cookie");
// oldCookie may have been signed with "olds3cret", but still parses ok
const value = await cookie.parse(oldCookie);
return data("...", {
headers: {
// Set-Cookie is signed with "n3wsecr3t"
"Set-Cookie": await cookie.serialize(value),
},
});
}
```
### Additional cookie utils
There are also several other cookie utilities available if you need them:
- [`isCookie`][is-cookie]
- [`createCookie`][create-cookie]
To learn more about each attribute, please see the [MDN Set-Cookie docs][cookie-attrs].
[csrf]: https://developer.mozilla.org/en-US/docs/Glossary/CSRF
[cookies]: #cookies
[sessions]: #sessions
[session-storage]: https://api.reactrouter.com/v7/interfaces/react_router.SessionStorage
[session-api]: https://api.reactrouter.com/v7/interfaces/react_router.Session
[is-session]: https://api.reactrouter.com/v7/functions/react_router.isSession
[cookie-api]: https://api.reactrouter.com/v7/interfaces/react_router.Cookie
[create-session-storage]: https://api.reactrouter.com/v7/functions/react_router.createSessionStorage
[create-session]: https://api.reactrouter.com/v7/functions/react_router.createSession
[create-memory-session-storage]: https://api.reactrouter.com/v7/functions/react_router.createMemorySessionStorage
[create-file-session-storage]: https://api.reactrouter.com/v7/functions/_react_router_node.createFileSessionStorage
[create-workers-kv-session-storage]: https://api.reactrouter.com/v7/functions/_react_router_cloudflare.createWorkersKVSessionStorage
[create-arc-table-session-storage]: https://api.reactrouter.com/v7/functions/_react_router_architect.createArcTableSessionStorage
[cookie]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
[cookie-attrs]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
[is-cookie]: https://api.reactrouter.com/v7/functions/react_router.isCookie
[create-cookie]: https://api.reactrouter.com/v7/functions/react_router.createCookie
---
title: Progressive Enhancement
---
# Progressive Enhancement
> Progressive enhancement is a strategy in web design that puts emphasis on web content first, allowing everyone to access the basic content and functionality of a web page, whilst users with additional browser features or faster Internet access receive the enhanced version instead.
<cite>- [Wikipedia][wikipedia]</cite>
When using React Router with Server-Side Rendering (the default in framework mode), you can automatically leverage the benefits of progressive enhancement.
## Why Progressive Enhancement Matters
Coined in 2003 by Steven Champeon & Nick Finck, the phrase emerged during a time of varied CSS and JavaScript support across different browsers, with many users actually browsing the web with JavaScript disabled.
Today, we are fortunate to develop for a much more consistent web and where the majority of users have JavaScript enabled.
However, we still believe in the core principles of progressive enhancement in React Router. It leads to fast and resilient apps with simple development workflows.
**Performance**: While it's easy to think that only 5% of your users have slow connections, the reality is that 100% of your users have slow connections 5% of the time.
**Resilience**: Everybody has JavaScript disabled until it's loaded.
**Simplicity**: Building your apps in a progressively enhanced way with React Router is actually simpler than building a traditional SPA.
## Performance
Server rendering allows your app to do more things in parallel than a typical [Single Page App (SPA)][spa], making the initial loading experience and subsequent navigations faster.
Typical SPAs send a blank document and only start doing work when JavaScript has loaded:
```
HTML |---|
JavaScript |---------|
Data |---------------|
page rendered 👆
```
A React Router app can start doing work the moment the request hits the server and stream the response so that the browser can start downloading JavaScript, other assets, and data in parallel:
```
👇 first byte
HTML |---|-----------|
JavaScript |---------|
Data |---------------|
page rendered 👆
```
## Resilience and Accessibility
While your users probably don't browse the web with JavaScript disabled, everybody uses the websites without JavaScript before it finishes loading. React Router embraces progressive enhancement by building on top of HTML, allowing you to build your app in a way that works without JavaScript, and then layer on JavaScript to enhance the experience.
The simplest case is a `<Link to="/account">`. These render an `<a href="/account">` tag that works without JavaScript. When JavaScript loads, React Router will intercept clicks and handle the navigation with client side routing. This gives you more control over the UX instead of just spinning favicons in the browser tab--but it works either way.
Now consider a simple add to cart button:
```tsx
export function AddToCart({ id }) {
return (
<Form method="post" action="/add-to-cart">
<input type="hidden" name="id" value={id} />
<button type="submit">Add To Cart</button>
</Form>
);
}
```
Whether JavaScript has loaded or not doesn't matter, this button will add the product to the cart.
When JavaScript loads, React Router will intercept the form submission and handle it client side. This allows you to add your own pending UI, or other client side behavior.
## Simplicity
When you start to rely on basic features of the web like HTML and URLs, you will find that you reach for client side state and state management much less.
Consider the button from before, with no fundamental change to the code, we can pepper in some client side behavior:
```tsx lines=[1,4,7,10-12,14]
import { useFetcher } from "react-router";
export function AddToCart({ id }) {
const fetcher = useFetcher();
return (
<fetcher.Form method="post" action="/add-to-cart">
<input name="id" value={id} />
<button type="submit">
{fetcher.state === "submitting"
? "Adding..."
: "Add To Cart"}
</button>
</fetcher.Form>
);
}
```
This feature continues to work the very same as it did before when JavaScript is loading, but once JavaScript loads:
- `useFetcher` no longer causes a navigation like `<Form>` does, so the user can stay on the same page and keep shopping
- The app code determines the pending UI instead of spinning favicons in the browser
It's not about building it two different ways–once for JavaScript and once without–it's about building it in iterations. Start with the simplest version of the feature and ship it; then iterate to an enhanced user experience.
Not only will the user get a progressively enhanced experience, but the app developer gets to "progressively enhance" the UI without changing the fundamental design of the feature.
Another example where progressive enhancement leads to simplicity is with the URL. When you start with a URL, you don't need to worry about client side state management. You can just use the URL as the source of truth for the UI.
```tsx
export function SearchBox() {
return (
<Form method="get" action="/search">
<input type="search" name="query" />
<SearchIcon />
</Form>
);
}
```
This component doesn't need any state management. It just renders a form that submits to `/search`. When JavaScript loads, React Router will intercept the form submission and handle it client side. Here's the next iteration:
```tsx lines=[1,4-6,11]
import { useNavigation } from "react-router";
export function SearchBox() {
const navigation = useNavigation();
const isSearching =
navigation.location.pathname === "/search";
return (
<Form method="get" action="/search">
<input type="search" name="query" />
{isSearching ? <Spinner /> : <SearchIcon />}
</Form>
);
}
```
No fundamental change in architecture, simply a progressive enhancement for both the user and the code.
See also: [State Management][state_management]
[wikipedia]: https://en.wikipedia.org/wiki/Progressive_enhancement
[spa]: ../how-to/spa
[state_management]: ./state-management
---
title: Hydration
hidden: true
---
There are a few nuances worth noting around the behavior of `HydrateFallback`:
- It is only relevant on initial document request and hydration, and will not be rendered on any subsequent client-side navigations
- It is only relevant when you are also setting [`clientLoader.hydrate=true`][hydrate-true] on a given route
- It is also relevant if you do have a `clientLoader` without a server `loader`, as this implies `clientLoader.hydrate=true` since there is otherwise no loader data at all to return from `useLoaderData`
- Even if you do not specify a `HydrateFallback` in this case, React Router will not render your route component and will bubble up to any ancestor `HydrateFallback` component
- This is to ensure that `useLoaderData` remains "happy-path"
- Without a server `loader`, `useLoaderData` would return `undefined` in any rendered route components
- You cannot render an `<Outlet/>` in a `HydrateFallback` because children routes can't be guaranteed to operate correctly since their ancestor loader data may not yet be available if they are running `clientLoader` functions on hydration (i.e., use cases such as `useRouteLoaderData()` or `useMatches()`)
---
title: Location Object
hidden: true
---
<!-- put some stuff about what it is and how it can be used, probably good opportunity for a couple how-tos as well with scroll restoration, etc -->
---
title: Route Matching
hidden: true
# want to explain how the matching algorithm works with any potential gotchas
---
# Route Matching
---
title: Automatic Code Splitting
---
# Automatic Code Splitting
When using React Router's framework features, your application is automatically code split to improve the performance of initial load times when users visit your application.
## Code Splitting by Route
Consider this simple route config:
```tsx filename=app/routes.ts
import {
type RouteConfig,
route,
} from "@react-router/dev/routes";
export default [
route("/contact", "./contact.tsx"),
route("/about", "./about.tsx"),
] satisfies RouteConfig;
```
Instead of bundling all routes into a single giant build, the modules referenced (`contact.tsx` and `about.tsx`) become entry points to the bundler.
Because these entry points are coupled to URL segments, React Router knows just from a URL which bundles are needed in the browser, and more importantly, which are not.
If the user visits `"/about"` then the bundles for `about.tsx` will be loaded but not `contact.tsx`. This ensures drastically reduces the JavaScript footprint for initial page loads and speeds up your application.
## Removal of Server Code
Any server-only [Route Module APIs][route-module] will be removed from the bundles. Consider this route module:
```tsx
export async function loader() {
return { message: "hello" };
}
export async function action() {
console.log(Date.now());
return { ok: true };
}
export async function headers() {
return { "Cache-Control": "max-age=300" };
}
export default function Component({ loaderData }) {
return <div>{loaderData.message}</div>;
}
```
After building for the browser, only the `Component` will still be in the bundle, so you can use server-only code in the other module exports.
[route-module]: ../../start/framework/route-module
---
title: Race Conditions
---
# Race Conditions
While impossible to eliminate every possible race condition in your application, React Router automatically handles the most common race conditions found in web user interfaces.
## Browser Behavior
React Router's handling of network concurrency is heavily inspired by the behavior of web browsers when processing documents.
Consider clicking a link to a new document, and then clicking a different link before the new page has finished loading. The browser will:
1. cancel the first request
2. immediately process the new navigation
The same behavior applies to form submissions. When a pending form submission is interrupted by a new one, the first is canceled and the new submission is immediately processed.
## React Router Behavior
Like the browser, interrupted navigations with links and form submissions will cancel in flight data requests and immediately process the new event.
Fetchers are a bit more nuanced since they are not singleton events like navigation. Fetchers can't interrupt other fetcher instances, but they can interrupt themselves and the behavior is the same as everything else: cancel the interrupted request and immediately process the new one.
Fetchers do, however, interact with each other when it comes to revalidation. After a fetcher's action request returns to the browser, a revalidation for all page data is sent. This means multiple revalidation requests can be in-flight at the same time. React Router will commit all "fresh" revalidation responses and cancel any stale requests. A stale request is any request that started _earlier_ than one that has returned.
This management of the network prevents the most common UI bugs caused by network race conditions.
Since networks are unpredictable, and your server still processes these cancelled requests, your backend may still experience race conditions and have potential data integrity issues. These risks are the same risks as using default browser behavior with plain HTML `<forms>`, which we consider to be low, and outside the scope of React Router.
## Practical Benefits
Consider building a type-ahead combobox. As the user types, you send a request to the server. As they type each new character you send a new request. It's important to not show the user results for a value that's not in the text field anymore.
When using a fetcher, this is automatically managed for you. Consider this pseudo-code:
```tsx
// route("/city-search", "./search-cities.ts")
export async function loader({ request }) {
const { searchParams } = new URL(request.url);
return searchCities(searchParams.get("q"));
}
```
```tsx
export function CitySearchCombobox() {
const fetcher = useFetcher();
return (
<fetcher.Form action="/city-search">
<Combobox aria-label="Cities">
<ComboboxInput
name="q"
onChange={(event) =>
// submit the form onChange to get the list of cities
fetcher.submit(event.target.form)
}
/>
{fetcher.data ? (
<ComboboxPopover className="shadow-popup">
{fetcher.data.length > 0 ? (
<ComboboxList>
{fetcher.data.map((city) => (
<ComboboxOption
key={city.id}
value={city.name}
/>
))}
</ComboboxList>
) : (
<span>No results found</span>
)}
</ComboboxPopover>
) : null}
</Combobox>
</fetcher.Form>
);
}
```
Calls to `fetcher.submit` will cancel pending requests on that fetcher automatically. This ensures you never show the user results for a request for a different input value.
---
title: State Management
---
# State Management
State management in React typically involves maintaining a synchronized cache of server data on the client side. However, when using React Router as your framework, most of the traditional caching solutions become redundant because of how it inherently handles data synchronization.
## Understanding State Management in React
In a typical React context, when we refer to "state management", we're primarily discussing how we synchronize server state with the client. A more apt term could be "cache management" because the server is the source of truth and the client state is mostly functioning as a cache.
Popular caching solutions in React include:
- **Redux:** A predictable state container for JavaScript apps.
- **React Query:** Hooks for fetching, caching, and updating asynchronous data in React.
- **Apollo:** A comprehensive state management library for JavaScript that integrates with GraphQL.
In certain scenarios, using these libraries may be warranted. However, with React Router's unique server-focused approach, their utility becomes less prevalent. In fact, most React Router applications forgo them entirely.
## How React Router Simplifies State
React Router seamlessly bridges the gap between the backend and frontend via mechanisms like loaders, actions, and forms with automatic synchronization through revalidation. This offers developers the ability to directly use server state within components without managing a cache, the network communication, or data revalidation, making most client-side caching redundant.
Here's why using typical React state patterns might be an anti-pattern in React Router:
1. **Network-related State:** If your React state is managing anything related to the network—such as data from loaders, pending form submissions, or navigational states—it's likely that you're managing state that React Router already manages:
- **[`useNavigation`][use_navigation]**: This hook gives you access to `navigation.state`, `navigation.formData`, `navigation.location`, etc.
- **[`useFetcher`][use_fetcher]**: This facilitates interaction with `fetcher.state`, `fetcher.formData`, `fetcher.data` etc.
- **[`loaderData`][loader_data]**: Access the data for a route.
- **[`actionData`][action_data]**: Access the data from the latest action.
2. **Storing Data in React Router:** A lot of data that developers might be tempted to store in React state has a more natural home in React Router, such as:
- **URL Search Params:** Parameters within the URL that hold state.
- **[Cookies][cookies]:** Small pieces of data stored on the user's device.
- **[Server Sessions][sessions]:** Server-managed user sessions.
- **Server Caches:** Cached data on the server side for quicker retrieval.
3. **Performance Considerations:** At times, client state is leveraged to avoid redundant data fetching. With React Router, you can use the [`Cache-Control`][cache_control_header] headers within `loader`s, allowing you to tap into the browser's native cache. However, this approach has its limitations and should be used judiciously. It's usually more beneficial to optimize backend queries or implement a server cache. This is because such changes benefit all users and do away with the need for individual browser caches.
As a developer transitioning to React Router, it's essential to recognize and embrace its inherent efficiencies rather than applying traditional React patterns. React Router offers a streamlined solution to state management leading to less code, fresh data, and no state synchronization bugs.
## Examples
### Network Related State
For examples on using React Router's internal state to manage network related state, refer to [Pending UI][pending_ui].
### URL Search Params
Consider a UI that lets the user customize between list view or detail view. Your instinct might be to reach for React state:
```tsx bad lines=[2,6,9]
export function List() {
const [view, setView] = useState("list");
return (
<div>
<div>
<button onClick={() => setView("list")}>
View as List
</button>
<button onClick={() => setView("details")}>
View with Details
</button>
</div>
{view === "list" ? <ListView /> : <DetailView />}
</div>
);
}
```
Now consider you want the URL to update when the user changes the view. Note the state synchronization:
```tsx bad lines=[7,16,24]
import { useNavigate, useSearchParams } from "react-router";
export function List() {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [view, setView] = useState(
searchParams.get("view") || "list"
);
return (
<div>
<div>
<button
onClick={() => {
setView("list");
navigate(`?view=list`);
}}
>
View as List
</button>
<button
onClick={() => {
setView("details");
navigate(`?view=details`);
}}
>
View with Details
</button>
</div>
{view === "list" ? <ListView /> : <DetailView />}
</div>
);
}
```
Instead of synchronizing state, you can simply read and set the state in the URL directly with boring old HTML forms:
```tsx good lines=[5,9-16]
import { Form, useSearchParams } from "react-router";
export function List() {
const [searchParams] = useSearchParams();
const view = searchParams.get("view") || "list";
return (
<div>
<Form>
<button name="view" value="list">
View as List
</button>
<button name="view" value="details">
View with Details
</button>
</Form>
{view === "list" ? <ListView /> : <DetailView />}
</div>
);
}
```
### Persistent UI State
Consider a UI that toggles a sidebar's visibility. We have three ways to handle the state:
1. React state
2. Browser local storage
3. Cookies
In this discussion, we'll break down the trade-offs associated with each method.
#### React State
React state provides a simple solution for temporary state storage.
**Pros**:
- **Simple**: Easy to implement and understand.
- **Encapsulated**: State is scoped to the component.
**Cons**:
- **Transient**: Doesn't survive page refreshes, returning to the page later, or unmounting and remounting the component.
**Implementation**:
```tsx
function Sidebar() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen((open) => !open)}>
{isOpen ? "Close" : "Open"}
</button>
<aside hidden={!isOpen}>
<Outlet />
</aside>
</div>
);
}
```
#### Local Storage
To persist state beyond the component lifecycle, browser local storage is a step-up. See our doc on [Client Data][client_data] for more advanced examples.
**Pros**:
- **Persistent**: Maintains state across page refreshes and component mounts/unmounts.
- **Encapsulated**: State is scoped to the component.
**Cons**:
- **Requires Synchronization**: React components must sync up with local storage to initialize and save the current state.
- **Server Rendering Limitation**: The [`window`][window_global] and [`localStorage`][local_storage_global] objects are not accessible during server-side rendering, so state must be initialized in the browser with an effect.
- **UI Flickering**: On initial page loads, the state in local storage may not match what was rendered by the server and the UI will flicker when JavaScript loads.
**Implementation**:
```tsx
function Sidebar() {
const [isOpen, setIsOpen] = useState(false);
// synchronize initially
useLayoutEffect(() => {
const isOpen = window.localStorage.getItem("sidebar");
setIsOpen(isOpen);
}, []);
// synchronize on change
useEffect(() => {
window.localStorage.setItem("sidebar", isOpen);
}, [isOpen]);
return (
<div>
<button onClick={() => setIsOpen((open) => !open)}>
{isOpen ? "Close" : "Open"}
</button>
<aside hidden={!isOpen}>
<Outlet />
</aside>
</div>
);
}
```