@rsc-kit/mcp 0.20.13 → 0.21.0

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/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
- ### useFormStatus
87
+ ### In a component of its own
72
88
 
73
- Nested components can access form state via context, without prop drilling:
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 { useFormStatus } from "@rsc-kit/core/form";
95
+ import { useForm } from "@rsc-kit/core/form";
79
96
 
80
97
  export function SubmitButton() {
81
- const { pending } = useFormStatus();
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, `<Form>` fills `errors`
96
- themselves — there is no `try`/`catch` to write, and no state to hold. `errors`
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, errors }) => (
219
+ {({ pending, error }) => (
202
220
  <>
203
221
  <Label htmlFor="title">Title</Label>
204
222
  <Input id="title" name="title" />
205
- {errors.title?.[0] && <p className="text-destructive">{errors.title[0]}</p>}
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, errors }) => (
267
+ {({ pending, error }) => (
250
268
  <FieldGroup>
251
- <Field data-invalid={!!errors.title}>
269
+ <Field data-invalid={!!error('title')}>
252
270
  <FieldLabel htmlFor="title">Bug title</FieldLabel>
253
- <Input id="title" name="title" aria-invalid={!!errors.title} />
271
+ <Input id="title" name="title" aria-invalid={!!error('title')} />
254
272
  <FieldDescription>Keep it short and specific.</FieldDescription>
255
- <FieldError errors={errors.title?.map((message) => ({ message }))} />
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` takes `Array<{ message?: string }>`, and our `errors` are
267
- `string[]` per field — hence the one `map`. Everything else is the same markup
268
- you would write with any other form library.
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 they are the client's
272
- **and** whatever the server sent back, in the same object, because a refused
273
- action returns its fields rather than throwing them away.
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 `errors['address.city']`.
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, pending, errors }) => (
397
- <Field data-invalid={!!errors.description}>
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 errors={errors.description?.map((message) => ({ message }))} />
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
- ### How a field is doing
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. `touched` is what separates "not filled in yet" from "filled in
452
- wrongly".
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/Form'
516
+ import { useField } from '@rsc-kit/core/form'
521
517
 
522
518
  function Title() {
523
- const { field, invalid, errors, ...bound } = useField('title')
519
+ const { field, error } = useField('title')
524
520
 
525
- return <Input {...bound} aria-invalid={invalid} />
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. What their design also gives is per-field meta,
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
- `useFormValues()` reads them from anywhere inside the form — a preview, a
540
- summary, a count of what has changed:
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 { title } = useFormValues<{ title: string }>()
541
+ const { field } = useField('title')
545
542
 
546
- return <h2>{title || 'Untitled'}</h2>
543
+ return <h2>{field.value || 'Untitled'}</h2>
547
544
  }
548
545
  ```
549
546
 
550
- Only **bound** values are here. An uncontrolled input's value belongs to the
551
- DOM, and nothing can know it changed without listening to it — bind a field with
552
- `field()` or `useField` and it appears.
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
- Both hooks read a context, so they work anywhere below `<Form>`. That is usually
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 { title } = useFormValues<{ title: string }>(store)
582
+ const { field } = useField('title', store)
586
583
 
587
- return <h1>{title || 'Untitled'}</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)` and `useFormValues(store)` take one explicitly;
597
- without one they read the context, which is what almost every form wants.
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')` and `fieldState` included, for a visitor with no
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 route
23
- tree, `vite.config.ts`, `.env` with `RSC_BACKEND` and a generated secret, the
24
- scripts — and prints the Go half for you to add:
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
@@ -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, and whether to include Tailwind. Every
18
- answer has a flag, so it also runs unattended:
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. The first render
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 awaited by every entry point
39
- a render can come through — the built server, the dev server, the
40
- prerender, a route's middleware check, a server action, an api route — so
41
- "before the first render" holds without the app knowing the list.
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. A `register()` that resolved is not called again; one
53
- that rejected is retried by the next request, so a database that was not up
54
- yet is asked again rather than leaving the process permanently refusing.
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
@@ -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"
@@ -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 decode, on intent.** Decoding a payload loads the client chunks the
79
- page names, and a landing page decoding its Sign in link for every visitor
80
- loaded the sign-in page's thirty chunks onto phones that never tapped. So
81
- the decode waits for the signal that says which link is meant: a pointer
82
- that has settled on the link for 100 ms, a press, a touch. Each of those
83
- leads its click by 60–300 ms — about a round trip — and the chunks load in
84
- that time rather than after the click.
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
@@ -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**, so anything that renames a
57
- component between the build and the server breaks every slot below it.
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.