@rsc-kit/mcp 0.20.13 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/recipes.js +121 -72
- package/dist/recipes.js.map +1 -1
- package/guides/coming-from-inertia.md +4 -4
- package/guides/coming-from-next.md +4 -3
- package/guides/deployment.md +15 -0
- package/guides/edge-caching.md +7 -2
- package/guides/forms.md +64 -67
- package/guides/go.md +4 -3
- package/guides/installation.md +4 -2
- package/guides/instrumentation.md +18 -9
- package/guides/laravel.md +8 -0
- package/guides/metadata.md +12 -0
- package/guides/navigation.md +15 -7
- package/guides/no-javascript.md +7 -0
- package/guides/ppr.md +15 -2
- package/guides/pwa.md +193 -2
- package/guides/quick-start.md +12 -3
- package/guides/static-generation.md +4 -0
- package/guides/validation.md +6 -6
- package/guides/where-it-runs.md +5 -0
- package/package.json +1 -1
package/guides/forms.md
CHANGED
|
@@ -53,6 +53,22 @@ export default function TodoForm() {
|
|
|
53
53
|
|
|
54
54
|
The generic type parameter `<FormValues>` gives you autocomplete on `error()` and typed form data throughout the component.
|
|
55
55
|
|
|
56
|
+
### Two words: `error` and `field`
|
|
57
|
+
|
|
58
|
+
There is one way to read a form, and it is the same everywhere — in the render
|
|
59
|
+
prop, in a component of its own, with shadcn or without:
|
|
60
|
+
|
|
61
|
+
- **`error('title')`** — the field's message, or `undefined`. Show it, or test
|
|
62
|
+
it: `!!error('title')` is "invalid". Nested fields are paths:
|
|
63
|
+
`error('address.city')`, `error('items[0].name')`.
|
|
64
|
+
- **`field('title')`** — a binding to spread onto a control, for the few fields
|
|
65
|
+
whose value this component has to hold. Most fields need nothing: they are
|
|
66
|
+
uncontrolled, the DOM holds the value, and it is read back on submit.
|
|
67
|
+
|
|
68
|
+
Beside them: `pending`, `dirty`, `succeeded`, `recentlySucceeded`, `reset()` and
|
|
69
|
+
`clearErrors()`. A field is checked when it is left and when the form is
|
|
70
|
+
submitted, so `error()` never lights up on a field nobody has reached.
|
|
71
|
+
|
|
56
72
|
### Props
|
|
57
73
|
|
|
58
74
|
```text
|
|
@@ -68,17 +84,18 @@ replace — replace history state (GET forms)
|
|
|
68
84
|
preserveScroll — keep scroll position (GET forms)
|
|
69
85
|
```
|
|
70
86
|
|
|
71
|
-
###
|
|
87
|
+
### In a component of its own
|
|
72
88
|
|
|
73
|
-
|
|
89
|
+
`useForm()` returns exactly what the render prop gets, so a piece of a form
|
|
90
|
+
moved into its own component is written the same way it was inline:
|
|
74
91
|
|
|
75
92
|
```tsx title="SubmitButton.tsx"
|
|
76
93
|
"use client";
|
|
77
94
|
|
|
78
|
-
import {
|
|
95
|
+
import { useForm } from "@rsc-kit/core/form";
|
|
79
96
|
|
|
80
97
|
export function SubmitButton() {
|
|
81
|
-
const { pending } =
|
|
98
|
+
const { pending } = useForm();
|
|
82
99
|
|
|
83
100
|
return (
|
|
84
101
|
<button type="submit" disabled={pending}>
|
|
@@ -88,14 +105,15 @@ export function SubmitButton() {
|
|
|
88
105
|
}
|
|
89
106
|
```
|
|
90
107
|
|
|
108
|
+
Not React's `useFormStatus`: that is a different hook with a different answer,
|
|
109
|
+
which is why this one is not called that.
|
|
110
|
+
|
|
91
111
|
---
|
|
92
112
|
|
|
93
113
|
## Validation errors
|
|
94
114
|
|
|
95
|
-
When a submit fails validation,
|
|
96
|
-
|
|
97
|
-
is `Partial<Record<keyof T, string[]>>`: each field maps to its messages, and
|
|
98
|
-
`error('title')` returns the first one.
|
|
115
|
+
When a submit fails validation, `error('title')` answers with the field's
|
|
116
|
+
message — there is no `try`/`catch` to write, and no state to hold.
|
|
99
117
|
|
|
100
118
|
Where those errors come from depends on the host, and the components do not
|
|
101
119
|
care. See [Validation](/guides/validation).
|
|
@@ -198,11 +216,11 @@ any component that ends up rendering a real form control is already compatible:
|
|
|
198
216
|
|
|
199
217
|
```tsx
|
|
200
218
|
<Form action={createPost} schema={schema}>
|
|
201
|
-
{({ pending,
|
|
219
|
+
{({ pending, error }) => (
|
|
202
220
|
<>
|
|
203
221
|
<Label htmlFor="title">Title</Label>
|
|
204
222
|
<Input id="title" name="title" />
|
|
205
|
-
{
|
|
223
|
+
{error('title') && <p className="text-destructive">{error('title')}</p>}
|
|
206
224
|
|
|
207
225
|
<Select name="kind">
|
|
208
226
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
|
@@ -246,13 +264,13 @@ directly:
|
|
|
246
264
|
|
|
247
265
|
```tsx
|
|
248
266
|
<Form action={reportBug} schema={formSchema}>
|
|
249
|
-
{({ pending,
|
|
267
|
+
{({ pending, error }) => (
|
|
250
268
|
<FieldGroup>
|
|
251
|
-
<Field data-invalid={!!
|
|
269
|
+
<Field data-invalid={!!error('title')}>
|
|
252
270
|
<FieldLabel htmlFor="title">Bug title</FieldLabel>
|
|
253
|
-
<Input id="title" name="title" aria-invalid={!!
|
|
271
|
+
<Input id="title" name="title" aria-invalid={!!error('title')} />
|
|
254
272
|
<FieldDescription>Keep it short and specific.</FieldDescription>
|
|
255
|
-
<FieldError
|
|
273
|
+
<FieldError>{error('title')}</FieldError>
|
|
256
274
|
</Field>
|
|
257
275
|
|
|
258
276
|
<Button type="submit" disabled={pending}>
|
|
@@ -263,14 +281,14 @@ directly:
|
|
|
263
281
|
</Form>
|
|
264
282
|
```
|
|
265
283
|
|
|
266
|
-
`FieldError`
|
|
267
|
-
`
|
|
268
|
-
|
|
284
|
+
`FieldError` renders its children, and renders nothing when there are none —
|
|
285
|
+
so `error('title')` goes straight in. Everything else is the same markup you
|
|
286
|
+
would write with any other form library.
|
|
269
287
|
|
|
270
288
|
The difference is where the errors came from. With TanStack Form or
|
|
271
|
-
react-hook-form those are the *client's* validation; here
|
|
272
|
-
**and** whatever the server sent back,
|
|
273
|
-
|
|
289
|
+
react-hook-form those are the *client's* validation; here `error()` answers with
|
|
290
|
+
the client's **and** whatever the server sent back, because a refused action
|
|
291
|
+
returns its fields rather than throwing them away.
|
|
274
292
|
|
|
275
293
|
### Setting the values
|
|
276
294
|
|
|
@@ -372,7 +390,7 @@ Names that describe a shape build it:
|
|
|
372
390
|
|
|
373
391
|
Which is the shape your schema was written against — and the shape whose errors
|
|
374
392
|
come back keyed the same way, because Standard Schema issue paths join with
|
|
375
|
-
dots too. A refused `address.city` is `
|
|
393
|
+
dots too. A refused `address.city` is `error('address.city')`.
|
|
376
394
|
|
|
377
395
|
Rows you add and remove are ordinary state; only the *names* have to line up:
|
|
378
396
|
|
|
@@ -393,8 +411,8 @@ react-hook-form's `<Controller>` render props:
|
|
|
393
411
|
|
|
394
412
|
```tsx
|
|
395
413
|
<Form action={reportBug} schema={formSchema} defaultValues={{ description: '' }}>
|
|
396
|
-
{({ field,
|
|
397
|
-
<Field data-invalid={!!
|
|
414
|
+
{({ field, error }) => (
|
|
415
|
+
<Field data-invalid={!!error('description')}>
|
|
398
416
|
<FieldLabel htmlFor="description">Description</FieldLabel>
|
|
399
417
|
|
|
400
418
|
<InputGroup>
|
|
@@ -404,7 +422,7 @@ react-hook-form's `<Controller>` render props:
|
|
|
404
422
|
</InputGroupAddon>
|
|
405
423
|
</InputGroup>
|
|
406
424
|
|
|
407
|
-
<FieldError
|
|
425
|
+
<FieldError>{error('description')}</FieldError>
|
|
408
426
|
</Field>
|
|
409
427
|
)}
|
|
410
428
|
</Form>
|
|
@@ -421,35 +439,13 @@ source of truth, and nothing merges.
|
|
|
421
439
|
Mix freely. Bind the one field that needs a character count and leave the rest
|
|
422
440
|
alone.
|
|
423
441
|
|
|
424
|
-
###
|
|
425
|
-
|
|
426
|
-
`fieldState(name)` is the other half — what is *known* about a field, as
|
|
427
|
-
opposed to what is spread onto it:
|
|
428
|
-
|
|
429
|
-
```tsx
|
|
430
|
-
{({ field, fieldState }) => {
|
|
431
|
-
const title = fieldState('title')
|
|
432
|
-
|
|
433
|
-
return (
|
|
434
|
-
<Field data-invalid={title.invalid}>
|
|
435
|
-
<FieldLabel htmlFor="title">Bug title</FieldLabel>
|
|
436
|
-
<Input id="title" {...field('title')} aria-invalid={title.invalid} />
|
|
437
|
-
<FieldError errors={title.errors.map((message) => ({ message }))} />
|
|
438
|
-
</Field>
|
|
439
|
-
)
|
|
440
|
-
}}
|
|
441
|
-
```
|
|
442
|
-
|
|
443
|
-
Two objects rather than one, which is react-hook-form's split and it is right
|
|
444
|
-
for a mechanical reason: `touched` and `invalid` are not DOM attributes, so a
|
|
445
|
-
single spreadable object would put them on the element and React would warn
|
|
446
|
-
about every one.
|
|
442
|
+
### When a field is checked
|
|
447
443
|
|
|
448
444
|
**A field is checked when it is left, not as it is typed.** An error that
|
|
449
445
|
appears while someone is halfway through an email address is a form arguing
|
|
450
446
|
with them; leaving the field is the moment they have finished saying what they
|
|
451
|
-
meant. `
|
|
452
|
-
|
|
447
|
+
meant. So `error()` has nothing for a field nobody has reached yet, and there is
|
|
448
|
+
no separate "touched" to consult before showing it.
|
|
453
449
|
|
|
454
450
|
It works on ordinary uncontrolled fields too — the form listens for `focusout`
|
|
455
451
|
rather than each field listening for `blur`, so `<Input name="title" />` is
|
|
@@ -517,42 +513,43 @@ one or two, for a character count or a control with no native element.
|
|
|
517
513
|
When it is not, put the field in its own component and subscribe there:
|
|
518
514
|
|
|
519
515
|
```tsx
|
|
520
|
-
import { useField } from '@rsc-kit/core/
|
|
516
|
+
import { useField } from '@rsc-kit/core/form'
|
|
521
517
|
|
|
522
518
|
function Title() {
|
|
523
|
-
const { field,
|
|
519
|
+
const { field, error } = useField('title')
|
|
524
520
|
|
|
525
|
-
return <Input {...
|
|
521
|
+
return <Input {...field} aria-invalid={!!error} />
|
|
526
522
|
}
|
|
527
523
|
```
|
|
528
524
|
|
|
525
|
+
The same two words, for one name: `field` to spread, `error` to show.
|
|
526
|
+
|
|
529
527
|
`useField` re-renders **that component and nothing else** — not the form, not
|
|
530
528
|
its siblings. Which is what `<Controller>` achieves with a render prop, except
|
|
531
529
|
that the component you were going to write anyway is the boundary.
|
|
532
530
|
|
|
533
531
|
So the scoping is there when a form is large enough to need it, and the flat
|
|
534
|
-
markup is there when it is not.
|
|
535
|
-
and that needed no render prop either: it is `fieldState()`.
|
|
532
|
+
markup is there when it is not.
|
|
536
533
|
|
|
537
534
|
### Reading the values from elsewhere
|
|
538
535
|
|
|
539
|
-
|
|
540
|
-
|
|
536
|
+
A preview, a summary, a count — something that shows a value without being the
|
|
537
|
+
field — reads it the same way the field does:
|
|
541
538
|
|
|
542
539
|
```tsx
|
|
543
540
|
function Preview() {
|
|
544
|
-
const {
|
|
541
|
+
const { field } = useField('title')
|
|
545
542
|
|
|
546
|
-
return <h2>{
|
|
543
|
+
return <h2>{field.value || 'Untitled'}</h2>
|
|
547
544
|
}
|
|
548
545
|
```
|
|
549
546
|
|
|
550
|
-
Only **bound**
|
|
551
|
-
DOM, and nothing can know it changed without listening to it —
|
|
552
|
-
`field()` or `useField` and it
|
|
547
|
+
Only a **bound** value can be read this way. An uncontrolled input's value
|
|
548
|
+
belongs to the DOM, and nothing can know it changed without listening to it —
|
|
549
|
+
bind the field with `field()` or `useField` and it can be read.
|
|
553
550
|
|
|
554
551
|
:::note[Inside the form, not outside it]
|
|
555
|
-
|
|
552
|
+
`useForm()` and `useField()` read a context, so they work anywhere below `<Form>`. That is usually
|
|
556
553
|
enough, because the `<form>` element can wrap as much of the page as you like,
|
|
557
554
|
and a submit button outside it is `form="the-id"`.
|
|
558
555
|
|
|
@@ -582,9 +579,9 @@ function Page() {
|
|
|
582
579
|
}
|
|
583
580
|
|
|
584
581
|
function TopBar({ store }) {
|
|
585
|
-
const {
|
|
582
|
+
const { field } = useField('title', store)
|
|
586
583
|
|
|
587
|
-
return <h1>{
|
|
584
|
+
return <h1>{field.value || 'Untitled'}</h1>
|
|
588
585
|
}
|
|
589
586
|
```
|
|
590
587
|
|
|
@@ -593,8 +590,8 @@ optimistic updates. Creating it does not subscribe to it, so the component
|
|
|
593
590
|
holding it does not re-render on every keystroke and take the whole subtree
|
|
594
591
|
with it.
|
|
595
592
|
|
|
596
|
-
`useField(name, store)`
|
|
597
|
-
|
|
593
|
+
`useField(name, store)` takes one explicitly; without one it reads the context,
|
|
594
|
+
which is what almost every form wants.
|
|
598
595
|
:::
|
|
599
596
|
|
|
600
597
|
## It works before hydration
|
|
@@ -609,7 +606,7 @@ hidden field and points the form at the page's own url, and the browser
|
|
|
609
606
|
posts there. The host reads the fields, runs the action they name exactly as
|
|
610
607
|
the enhanced path would have called it, and renders the page again with what
|
|
611
608
|
it returned seated in the form that posted — so a refusal shows on its
|
|
612
|
-
fields, `error('email')`
|
|
609
|
+
fields, `error('email')` included, for a visitor with no
|
|
613
610
|
javascript at all. A `redirect()` the action throws is followed as a
|
|
614
611
|
document's would be; a cookie it sets is on the answer. Same origin only, as
|
|
615
612
|
an action is, and the answer is never stored. A stored page is the one most
|
package/guides/go.md
CHANGED
|
@@ -19,9 +19,10 @@ go get github.com/rsc-kit/go
|
|
|
19
19
|
|
|
20
20
|
## Wiring
|
|
21
21
|
|
|
22
|
-
In a Go module, `init` sees `go.mod` and does the JavaScript half — the
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
In a Go module, `init` sees `go.mod` and does the JavaScript half — the
|
|
23
|
+
project's first `package.json` if it has none, the route tree,
|
|
24
|
+
`vite.config.ts`, `.env` with `RSC_BACKEND` and a generated secret, the scripts
|
|
25
|
+
— and prints the Go half for you to add. Nothing in the Go module is touched:
|
|
25
26
|
|
|
26
27
|
```sh
|
|
27
28
|
bunx rsc-kit@latest init # in the directory with go.mod
|
package/guides/installation.md
CHANGED
|
@@ -14,8 +14,10 @@ bun create rsc-kit@latest my-app
|
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
It asks where it will run (Bun, Node or Cloudflare Workers), whether you want the
|
|
17
|
-
React Compiler and which implementation,
|
|
18
|
-
|
|
17
|
+
React Compiler and which implementation, whether to include Tailwind, and
|
|
18
|
+
whether the app should be installable — offline, a manifest and starter icons
|
|
19
|
+
([`--pwa`](/guides/pwa#starting-one-installable)). Every answer has a flag, so it
|
|
20
|
+
also runs unattended:
|
|
19
21
|
|
|
20
22
|
```sh
|
|
21
23
|
bun create rsc-kit@latest my-app --host=bun --compiler=oxc --tailwind
|
|
@@ -11,8 +11,8 @@ evaluates, and before the first request.
|
|
|
11
11
|
// Runs at import, before any page: a bad variable stops the server here.
|
|
12
12
|
import './env'
|
|
13
13
|
|
|
14
|
-
// Anything asynchronous the app needs before it serves.
|
|
15
|
-
// waits for it.
|
|
14
|
+
// Anything asynchronous the app needs before it serves. Called once per
|
|
15
|
+
// process, at startup; the first render waits for it, later ones do not.
|
|
16
16
|
export async function register() {
|
|
17
17
|
await db.connect()
|
|
18
18
|
}
|
|
@@ -35,10 +35,13 @@ the first page module evaluates. Importing a bootstrap module from every
|
|
|
35
35
|
file that needs it works until someone forgets, and the failure is a page
|
|
36
36
|
that throws "not configured" for the one visitor who reached it first.
|
|
37
37
|
|
|
38
|
-
**Before the first request.** `register()` is
|
|
39
|
-
a render can come through — the built server, the dev server,
|
|
40
|
-
prerender, a route's middleware check, a server action, an api route —
|
|
41
|
-
|
|
38
|
+
**Before the first request.** `register()` is **called once**, and every
|
|
39
|
+
entry point a render can come through — the built server, the dev server,
|
|
40
|
+
the prerender, a route's middleware check, a server action, an api route —
|
|
41
|
+
waits on that one call rather than making its own. After startup it has long
|
|
42
|
+
since finished, so a request waits on nothing: it checks a promise that has
|
|
43
|
+
already settled. That is how "before the first render" holds without the app
|
|
44
|
+
knowing the list of entry points, and without a request ever paying for it.
|
|
42
45
|
|
|
43
46
|
## When it runs
|
|
44
47
|
|
|
@@ -49,9 +52,15 @@ prerender, a route's middleware check, a server action, an api route — so
|
|
|
49
52
|
| `vite dev` | when the dev server first evaluates the app, and again when the file or anything it imports changes |
|
|
50
53
|
| `vite build` | before the first page is prerendered |
|
|
51
54
|
|
|
52
|
-
Once per process
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
**Once per process, not once per request.** Measured on a built server and on
|
|
56
|
+
a compiled binary, with a `register()` that logs: one call before the first
|
|
57
|
+
request, and still one after a stream of them.
|
|
58
|
+
|
|
59
|
+
A `register()` that resolved is not called again. One that *rejected* is the
|
|
60
|
+
exception: the next request calls it again, so a database that was not up yet
|
|
61
|
+
is asked again rather than leaving the process permanently refusing. On a
|
|
62
|
+
server that case is rare in practice — a startup failure exits the process,
|
|
63
|
+
below.
|
|
55
64
|
|
|
56
65
|
On a server, a failure at startup — the file throwing at import, or
|
|
57
66
|
`register()` rejecting — is reported and the process exits. A server that
|
package/guides/laravel.md
CHANGED
|
@@ -375,6 +375,14 @@ server {
|
|
|
375
375
|
Laravel needs the renderer in `trustProxies` for `url()`, `route()` and its
|
|
376
376
|
redirects to come out against the public origin rather than its own.
|
|
377
377
|
|
|
378
|
+
Trusting it means trusting what it forwards, so the renderer drops any
|
|
379
|
+
`X-Forwarded-For`, `Forwarded` or `X-Real-IP` the browser sent: otherwise a
|
|
380
|
+
visitor would pick their own address for Laravel's throttles. The renderer
|
|
381
|
+
cannot see the peer address to put in its place, so Laravel sees the
|
|
382
|
+
renderer's. If the renderer sits behind a proxy of its own that writes those
|
|
383
|
+
headers (a load balancer, Cloudflare), set `RSC_TRUST_FORWARDED=1` on the
|
|
384
|
+
renderer and they are passed through.
|
|
385
|
+
|
|
378
386
|
Setting `RSC_RENDERER_URL` puts Laravel in front instead: it proxies anything
|
|
379
387
|
it does not route, which is what development does. In production it has the
|
|
380
388
|
cost the caution above describes, at scale — a worker is held for the whole
|
package/guides/metadata.md
CHANGED
|
@@ -172,6 +172,18 @@ correctly, but the structured objects are the ones to write: they are typed,
|
|
|
172
172
|
they take an image object with a width and height, and they are what a Next
|
|
173
173
|
app already has.
|
|
174
174
|
|
|
175
|
+
## On an iPhone's home screen
|
|
176
|
+
|
|
177
|
+
`appleWebApp` is Next's shape — `capable`, `title`, `statusBarStyle`,
|
|
178
|
+
`startupImage` — and renders the same tags. Launch screens are simpler as files
|
|
179
|
+
named by their size; see [Splash screens](/guides/pwa#splash-screens).
|
|
180
|
+
|
|
181
|
+
```tsx title="src/app/layout.tsx"
|
|
182
|
+
export const metadata: Metadata = {
|
|
183
|
+
appleWebApp: { capable: true, title: 'Orders', statusBarStyle: 'black-translucent' },
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
175
187
|
## The share card
|
|
176
188
|
|
|
177
189
|
```tsx title="src/app/layout.tsx"
|
package/guides/navigation.md
CHANGED
|
@@ -75,13 +75,21 @@ because the two cost differently:
|
|
|
75
75
|
same pictures are asked for again as urgent: a listing's tiles each
|
|
76
76
|
preload their product's picture, and the one tapped first was queued
|
|
77
77
|
behind the rest; asked for as high priority it moves to the front.
|
|
78
|
-
- **The
|
|
79
|
-
|
|
80
|
-
loaded the
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
78
|
+
- **The code for its client components, as the bytes land.** A payload names
|
|
79
|
+
each client component it renders, and the first eight a page renders are
|
|
80
|
+
loaded at idle the moment the payload arrives — the code only, nothing
|
|
81
|
+
rendered. Loaded at the touch instead, the first page of a visit waited on
|
|
82
|
+
its form's chunk *after* it had arrived, and showed its skeleton for a
|
|
83
|
+
quarter of a second; every later page found the chunk cached. Measured on an
|
|
84
|
+
iPhone, tapping through a fresh session: the first product page went from
|
|
85
|
+
~325 ms with a skeleton to ~80 ms without one. Bounded per document, so a
|
|
86
|
+
landing page with a link to a heavy editor does not load the editor for
|
|
87
|
+
everyone.
|
|
88
|
+
- **The decode, on intent.** Decoding the payload itself waits for the signal
|
|
89
|
+
that says which link is meant: a pointer that has settled on the link for
|
|
90
|
+
100 ms, a press, a touch. Each of those leads its click by 60–300 ms — about
|
|
91
|
+
a round trip — which is when the page is rendered hidden, so the click
|
|
92
|
+
reveals it.
|
|
85
93
|
|
|
86
94
|
The click then reads a decoded tree, changes the url, and hands the page to
|
|
87
95
|
React. Nothing on the network is between the click and the page unless the
|
package/guides/no-javascript.md
CHANGED
|
@@ -39,6 +39,13 @@ document itself. The build says so on the route's line. Fonts are the one
|
|
|
39
39
|
thing left to you, because only you know which face the first paint needs —
|
|
40
40
|
the [fonts guide](/guides/fonts) shows the two-line preload.
|
|
41
41
|
|
|
42
|
+
Every other stored page gets the same, shells included. One that hydrates
|
|
43
|
+
keeps its `<link>` as well, turned to `media="print"`: React looks for its
|
|
44
|
+
stylesheets by href and would fetch a missing one again. The browser loads
|
|
45
|
+
the print link at the lowest priority without holding up a paint, and never
|
|
46
|
+
applies it on screen. A page rendered per request is not stored, and keeps
|
|
47
|
+
its link.
|
|
48
|
+
|
|
42
49
|
The default is not always the right call. A site of many small pages — a
|
|
43
50
|
blog, documentation — may prefer one stylesheet the browser caches once over
|
|
44
51
|
the same 4 kB repeated in every document, and a sheet just over the cap may
|
package/guides/ppr.md
CHANGED
|
@@ -53,8 +53,17 @@ a url it does know; the client sets the title again after hydration. Static
|
|
|
53
53
|
|
|
54
54
|
## A shell and the server that finishes it are one build
|
|
55
55
|
|
|
56
|
-
A replay matches a slot by component **name
|
|
57
|
-
component between the build and the server breaks
|
|
56
|
+
A replay matches a slot by component **name** — `displayName`, then `name` —
|
|
57
|
+
so anything that renames a component between the build and the server breaks
|
|
58
|
+
every slot below it. A bundler run over the build's output does exactly that
|
|
59
|
+
when it merges module scopes: two different components of one name is
|
|
60
|
+
ordinary (a wrapper named after the primitive it wraps), and the merge renames
|
|
61
|
+
one of them.
|
|
62
|
+
|
|
63
|
+
The build pins every component's name as it is in the build's output, as a
|
|
64
|
+
`displayName` written into the server bundle. Whatever a later bundler calls
|
|
65
|
+
the binding, React reads the name the shell recorded — which is why a page
|
|
66
|
+
compiled into a single binary is resumed like any other, with no flag.
|
|
58
67
|
|
|
59
68
|
|
|
60
69
|
A postponed state names the components React has to find when it replays, and
|
|
@@ -242,6 +251,10 @@ is the fix.
|
|
|
242
251
|
`RSC_PPR_TIMEOUT_MS` sets it; the default is 2 seconds. Raising it lets slow
|
|
243
252
|
pages be stored whole; lowering it pushes more of them to shells.
|
|
244
253
|
|
|
254
|
+
The budget is per page, but pages do not wait in line for it. A page that has
|
|
255
|
+
rendered what it can and is only waiting gives its place to the next, so a
|
|
256
|
+
site of hundreds of shells costs about one budget, not one each.
|
|
257
|
+
|
|
245
258
|
A page whose data resolves inside the budget is stored **whole**, even one that
|
|
246
259
|
called `fetch()`. If a page must reflect the world at request time, what makes
|
|
247
260
|
it so is reading the request — not a flag.
|