@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/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/dist/recipes.js
CHANGED
|
@@ -15,28 +15,37 @@
|
|
|
15
15
|
const RECIPES = [
|
|
16
16
|
{
|
|
17
17
|
topic: 'forms',
|
|
18
|
-
summary: 'Submitting to a server action, with pending state and field errors. Uncontrolled by default
|
|
18
|
+
summary: 'Submitting to a server action, with pending state and field errors. Uncontrolled by default; read with error() and field()',
|
|
19
19
|
body: `THE RULE: forms are UNCONTROLLED. Inputs keep their value in the DOM,
|
|
20
20
|
an initial value is defaultValue, the action reads FormData. Do NOT write
|
|
21
21
|
useState + value/onChange per input, and do NOT reach for TanStack Form.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
|
|
23
|
+
ONE WAY TO READ A FORM - two words, the same everywhere:
|
|
24
|
+
error('title') the field's error, or undefined. !!error('title') is invalid.
|
|
25
|
+
Nested fields are paths: error('address.city'), error('items[0].name').
|
|
26
|
+
field('title') a binding to spread - ONLY for a field whose value the UI must
|
|
27
|
+
hold (a character count, a live preview, a control with no
|
|
28
|
+
native element). Everything else stays uncontrolled.
|
|
29
|
+
Beside them: pending, dirty, succeeded, recentlySucceeded, reset(), clearErrors(),
|
|
30
|
+
and data (what is being submitted).
|
|
31
|
+
|
|
32
|
+
There is nothing else. No errors map, no fieldState, no useFormValues, no
|
|
33
|
+
useFormStatus - those were removed; if you see them in old code, convert them.
|
|
25
34
|
|
|
26
35
|
Use <Form>. It takes the server action itself, not a url.
|
|
27
36
|
|
|
28
37
|
\`\`\`tsx
|
|
29
38
|
'use client'
|
|
30
|
-
import Form from '@rsc-kit/core/
|
|
39
|
+
import Form from '@rsc-kit/core/form'
|
|
31
40
|
import { createPost } from '../actions'
|
|
32
41
|
|
|
33
42
|
export function NewPost() {
|
|
34
43
|
return (
|
|
35
44
|
<Form action={createPost} schema={schema}>
|
|
36
|
-
{({ pending,
|
|
45
|
+
{({ pending, error }) => (
|
|
37
46
|
<>
|
|
38
47
|
<input name="title" />
|
|
39
|
-
{
|
|
48
|
+
{error('title') && <p>{error('title')}</p>}
|
|
40
49
|
<button disabled={pending}>Save</button>
|
|
41
50
|
</>
|
|
42
51
|
)}
|
|
@@ -45,6 +54,20 @@ export function NewPost() {
|
|
|
45
54
|
}
|
|
46
55
|
\`\`\`
|
|
47
56
|
|
|
57
|
+
In a child component, useForm() returns EXACTLY what the render prop gets, so
|
|
58
|
+
the code is the same:
|
|
59
|
+
|
|
60
|
+
\`\`\`tsx
|
|
61
|
+
import { useForm } from '@rsc-kit/core/form'
|
|
62
|
+
|
|
63
|
+
function SubmitButton() {
|
|
64
|
+
const { pending } = useForm()
|
|
65
|
+
return <button disabled={pending}>Save</button>
|
|
66
|
+
}
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
(Not React's useFormStatus - a different hook with a different answer.)
|
|
70
|
+
|
|
48
71
|
Passing \`schema\` validates in the browser BEFORE the action is called, so a
|
|
49
72
|
mistake costs no round trip. It is a courtesy, never a control: the action is a
|
|
50
73
|
public endpoint reachable without your form, so the server must check too.
|
|
@@ -52,6 +75,10 @@ public endpoint reachable without your form, so the server must check too.
|
|
|
52
75
|
A schema on the server (\`client.input(schema)\`) does NOT give you client-side
|
|
53
76
|
validation. Pass it to the form as well — the same schema is fine.
|
|
54
77
|
|
|
78
|
+
A field is checked when it is LEFT, not as it is typed (the form listens for
|
|
79
|
+
focusout, so uncontrolled fields are covered), and on submit. So error() has
|
|
80
|
+
nothing for a field nobody reached - there is no "touched" to check first.
|
|
81
|
+
|
|
55
82
|
Values are uncontrolled, so an initial one is React's own \`defaultValue\`. A
|
|
56
83
|
refused submit keeps what was typed, because the DOM kept it.
|
|
57
84
|
|
|
@@ -65,85 +92,65 @@ z.array() accepts - so for anything that is a list by nature end the name in
|
|
|
65
92
|
|
|
66
93
|
Names that describe a shape build it: \`address.city\` nests, and
|
|
67
94
|
\`items[0].name\` (or \`items[0][name]\`) makes an array of objects. That is
|
|
68
|
-
the shape the schema was written against, and
|
|
69
|
-
way because Standard Schema issue paths join with dots too.
|
|
95
|
+
the shape the schema was written against, and error() takes the same path.
|
|
70
96
|
|
|
71
|
-
|
|
72
|
-
or a value read as it is typed, bind it with \`field()\`. It is the same four
|
|
73
|
-
props react-hook-form's Controller gives:
|
|
97
|
+
Binding a field - a rich editor, a Radix select, a value read as it is typed:
|
|
74
98
|
|
|
75
99
|
\`\`\`tsx
|
|
76
100
|
<Form action={save} defaultValues={{ body: '' }}>
|
|
77
|
-
{({ field }) => (
|
|
101
|
+
{({ field, error }) => (
|
|
78
102
|
<>
|
|
79
103
|
<Editor {...field('body')} />
|
|
80
104
|
<span>{field('body').value.length}/100</span>
|
|
105
|
+
{error('body') && <p>{error('body')}</p>}
|
|
81
106
|
</>
|
|
82
107
|
)}
|
|
83
108
|
</Form>
|
|
84
109
|
\`\`\`
|
|
85
110
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
arrives in FormData with the rest
|
|
111
|
+
field() gives { name, value, onChange, onBlur } - react-hook-form Controller's
|
|
112
|
+
four. onChange takes a DOM event OR a bare value. A bound field is still an
|
|
113
|
+
ordinary named input, so it arrives in FormData with the rest.
|
|
89
114
|
|
|
90
|
-
|
|
91
|
-
objects rather than one because touched and invalid are not DOM attributes and
|
|
92
|
-
spreading them would warn on every field.
|
|
115
|
+
shadcn Field components, with the two words:
|
|
93
116
|
|
|
94
117
|
\`\`\`tsx
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
<Input
|
|
98
|
-
<FieldError
|
|
118
|
+
<Field data-invalid={!!error('title')}>
|
|
119
|
+
<FieldLabel htmlFor="title">Title</FieldLabel>
|
|
120
|
+
<Input id="title" name="title" aria-invalid={!!error('title')} />
|
|
121
|
+
<FieldError>{error('title')}</FieldError>
|
|
99
122
|
</Field>
|
|
100
123
|
\`\`\`
|
|
101
124
|
|
|
102
|
-
|
|
103
|
-
uncontrolled fields too - the form listens for focusout rather than each field
|
|
104
|
-
listening for blur.
|
|
105
|
-
|
|
106
|
-
There is no per-field render prop component here, and that is deliberate.
|
|
107
|
-
TanStack Form is controlled-first, so it needs one - without per-field
|
|
108
|
-
subscriptions a keystroke re-renders every field. react-hook-form is
|
|
109
|
-
uncontrolled-first like this, and its Controller scopes the re-render of a
|
|
110
|
-
controlled field to itself.
|
|
111
|
-
|
|
112
|
-
field() is a function call instead, which keeps the markup flat and means a
|
|
113
|
-
bound field re-renders the form rather than only itself. Right for the one or
|
|
114
|
-
two controlled fields a form usually has.
|
|
125
|
+
FieldError renders its children and renders nothing when there are none.
|
|
115
126
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
A bound field re-renders the form. For a LARGE controlled form, put the field in
|
|
128
|
+
its own component and use useField - the same two words for one name, and it
|
|
129
|
+
re-renders that component and nothing else:
|
|
119
130
|
|
|
120
131
|
\`\`\`tsx
|
|
121
132
|
function Title() {
|
|
122
|
-
const {
|
|
123
|
-
|
|
124
|
-
return <Input {...bound} aria-invalid={invalid} />
|
|
133
|
+
const { field, error } = useField('title')
|
|
134
|
+
return <Input {...field} aria-invalid={!!error} />
|
|
125
135
|
}
|
|
126
136
|
\`\`\`
|
|
127
137
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
138
|
+
A preview or summary reads a bound value the same way:
|
|
139
|
+
\`const { field } = useField('title'); field.value\`. Only BOUND values can be
|
|
140
|
+
read - an uncontrolled input's value is the DOM's.
|
|
131
141
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
hand it in:
|
|
142
|
+
For something that is NOT a descendant - a top bar - create the store above both
|
|
143
|
+
and hand it in; useField(name, store) reads it:
|
|
135
144
|
|
|
136
145
|
\`\`\`tsx
|
|
137
146
|
const store = useFormStore({ title: '' })
|
|
138
147
|
|
|
139
|
-
<TopBar store={store} /> // outside the form
|
|
148
|
+
<TopBar store={store} /> // outside the form: useField('title', store)
|
|
140
149
|
<Form action={save} store={store}>…</Form>
|
|
141
150
|
\`\`\`
|
|
142
151
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
take the subtree with it. useField(name, store) and useFormValues(store) take
|
|
146
|
-
one explicitly; without one they read the context.
|
|
152
|
+
Creating the store does not subscribe to it, so the holder does not re-render
|
|
153
|
+
per keystroke.
|
|
147
154
|
|
|
148
155
|
A submit from outside the form is html, not a second api:
|
|
149
156
|
|
|
@@ -152,21 +159,14 @@ A submit from outside the form is html, not a second api:
|
|
|
152
159
|
<Button type="submit" form="bug-report">Submit</Button>
|
|
153
160
|
\`\`\`
|
|
154
161
|
|
|
155
|
-
There is no useForm hook. <Form> is the whole surface.
|
|
156
|
-
|
|
157
|
-
Fields are real \`name\` attributes rather than controlled state, so the form
|
|
158
|
-
reads a native FormData and any component rendering a real control works.
|
|
159
|
-
|
|
160
162
|
It works before hydration. The action is on the form element as well as in the
|
|
161
|
-
submit handler, so the markup is submittable on its own
|
|
162
|
-
|
|
163
|
-
submit, so exactly one path runs.
|
|
163
|
+
submit handler, so the markup is submittable on its own; a refusal shows on its
|
|
164
|
+
fields through error() even with no javascript.
|
|
164
165
|
|
|
165
166
|
**shadcn/ui works as-is.** Input, Textarea, Button and Label are styled native
|
|
166
167
|
elements, so \`name\` does what it always does. Select, Checkbox, Switch and
|
|
167
168
|
RadioGroup are Radix underneath and render a hidden native control whenever
|
|
168
|
-
given a \`name\` - omit it and they are invisible to the form
|
|
169
|
-
only thing to remember.
|
|
169
|
+
given a \`name\` - omit it and they are invisible to the form.
|
|
170
170
|
|
|
171
171
|
Do NOT use shadcn's own Form/FormField/FormControl with this. Those wrap
|
|
172
172
|
react-hook-form, a different system for the same job. One or the other.`,
|
|
@@ -246,7 +246,13 @@ payload and every held page is dropped - they are from before the write - so
|
|
|
246
246
|
visit() to a list after creating a row fetches the list with the row in it.
|
|
247
247
|
What an action re-rendered goes under the page it was invoked on: a tap that
|
|
248
248
|
left the page while the action was in flight is answered by fetching the page
|
|
249
|
-
now showing again, whole, never by putting the old page under the new url
|
|
249
|
+
now showing again, whole, never by putting the old page under the new url.
|
|
250
|
+
|
|
251
|
+
The client components a prefetched page renders are loaded (code only) as its
|
|
252
|
+
payload lands - the first eight per page, bounded per document - so the first
|
|
253
|
+
visit to a page does not wait on a chunk after the page arrived. Nothing to
|
|
254
|
+
configure. Serve over HTTP/2 (Cloudflare does by default): every client
|
|
255
|
+
component is its own small chunk.`,
|
|
250
256
|
},
|
|
251
257
|
{
|
|
252
258
|
topic: 'validation',
|
|
@@ -516,7 +522,13 @@ not found" - it is the package's, and fixed.`,
|
|
|
516
522
|
{
|
|
517
523
|
topic: 'pwa',
|
|
518
524
|
summary: 'Making the app installable',
|
|
519
|
-
body: `A
|
|
525
|
+
body: `A NEW app: bun create rsc-kit@latest my-app --pwa. It turns on offline and
|
|
526
|
+
writes src/app/manifest.ts, a static src/app/offline/page.tsx and starter icons
|
|
527
|
+
(icon-192.png, icon-512.png, icon-maskable-512.png, apple-icon.png) - Chrome
|
|
528
|
+
reports it installable with nothing missing. Replace the icons, keep the names.
|
|
529
|
+
Nothing in rsc-kit processes images.
|
|
530
|
+
|
|
531
|
+
An EXISTING app - a manifest file beside the routes:
|
|
520
532
|
|
|
521
533
|
\`\`\`ts title="src/app/manifest.ts"
|
|
522
534
|
import type { WebManifest } from '@rsc-kit/core/manifest-file'
|
|
@@ -537,11 +549,37 @@ imported from elsewhere.
|
|
|
537
549
|
favicon.ico served at /favicon.ico
|
|
538
550
|
icon-192.png <link rel="icon">, and the manifest's icons
|
|
539
551
|
icon-512.png
|
|
552
|
+
icon-maskable-512.png manifest only, purpose "maskable" (safe-zone padded)
|
|
540
553
|
apple-icon.png <link rel="apple-touch-icon">
|
|
554
|
+
apple-splash-1179x2556.png iOS launch screen for that device, portrait
|
|
541
555
|
opengraph-image.png <meta property="og:image">
|
|
542
556
|
twitter-image.png <meta name="twitter:image">
|
|
543
557
|
|
|
544
|
-
|
|
558
|
+
screenshot-wide-1280x720.png manifest screenshots, form_factor wide (desktop)
|
|
559
|
+
screenshot-narrow-750x1334.png form_factor narrow (phone) - Chrome's richer install sheet
|
|
560
|
+
|
|
561
|
+
Sizes are read from the filename; no image is ever opened or resized.
|
|
562
|
+
|
|
563
|
+
Set id: '/' in the manifest and never change it - without it the app's identity
|
|
564
|
+
is its startUrl, and changing that orphans every install (the build warns).
|
|
565
|
+
scope, orientation, categories and shortcuts are typed too.
|
|
566
|
+
|
|
567
|
+
An install button: useInstall() from '@rsc-kit/core/install' ->
|
|
568
|
+
{ canInstall, install, installed, ios }. The offer (beforeinstallprompt) is
|
|
569
|
+
caught by the page's inline bootstrap, so a button that mounts late still gets
|
|
570
|
+
it - do NOT add your own beforeinstallprompt listener in an effect, it can miss
|
|
571
|
+
the event. install() resolves 'accepted' | 'dismissed' | 'unavailable'. On iOS
|
|
572
|
+
there is no offer; when ios is true, tell the visitor Share -> Add to Home Screen.
|
|
573
|
+
|
|
574
|
+
**Splash screens.** Android draws its own from the manifest: name, the 512px
|
|
575
|
+
icon and backgroundColor - set all three. iOS needs one image per device and
|
|
576
|
+
orientation: name each apple-splash-WIDTHxHEIGHT.png at the device's pixel
|
|
577
|
+
size (landscape swaps them) and the build writes the media query from a device
|
|
578
|
+
table; a size no device has is named at build and linked nowhere. A Next port
|
|
579
|
+
can keep metadata.appleWebApp ({ capable, title, statusBarStyle, startupImage })
|
|
580
|
+
unchanged. read_guide({ slug: 'pwa' }) has the size table.
|
|
581
|
+
|
|
582
|
+
The build says whether it worked:
|
|
545
583
|
|
|
546
584
|
[rsc-kit] manifest: Orders is installable
|
|
547
585
|
[rsc-kit] manifest: no icons, so no browser will offer to install this.
|
|
@@ -598,7 +636,10 @@ still works: its flight payload is written with the wrappers.
|
|
|
598
636
|
|
|
599
637
|
A page without the runtime also gets its stylesheet inlined when small
|
|
600
638
|
(rscKit({ inlineStylesheets }) to change), and still registers the service
|
|
601
|
-
worker with one inlined line.
|
|
639
|
+
worker with one inlined line. So does every other stored page, shells
|
|
640
|
+
included; one that hydrates keeps its link as media="print" so React finds
|
|
641
|
+
it rather than fetching it again. Do not "fix" that link - it is not render
|
|
642
|
+
blocking, and removing it makes React download the sheet after hydration.
|
|
602
643
|
|
|
603
644
|
Do NOT restructure an app to chase this, and do NOT look for export const
|
|
604
645
|
clientJs - it does not exist. The size column says what each route costs; a
|
|
@@ -1296,9 +1337,11 @@ auth, policies, jobs - answering one private endpoint, and it keeps every
|
|
|
1296
1337
|
route of its own. The whole model, and how to build for it:
|
|
1297
1338
|
read_guide({ slug: 'backend-answered-pages' }).
|
|
1298
1339
|
|
|
1299
|
-
Go: in a Go module, rsc-kit init sees go.mod, writes the JS half
|
|
1300
|
-
|
|
1301
|
-
github.com/rsc-kit/go.
|
|
1340
|
+
Go: in a Go module, rsc-kit init sees go.mod, writes the JS half - the
|
|
1341
|
+
project's first package.json if it has none - and .env (RSC_BACKEND + a
|
|
1342
|
+
generated secret) and prints the Go wiring; go get github.com/rsc-kit/go.
|
|
1343
|
+
A new app: bun create rsc-kit --backend=<url>, or rsc-kit init in an empty
|
|
1344
|
+
directory, which starts one there.
|
|
1302
1345
|
|
|
1303
1346
|
A backend in another language answers that ONE endpoint, POST /__rsc/host-call,
|
|
1304
1347
|
and the renderer wires itself from two variables in .env: RSC_BACKEND (a
|
|
@@ -1558,8 +1601,8 @@ export const env = createEnv({
|
|
|
1558
1601
|
},
|
|
1559
1602
|
clientPrefix: 'PUBLIC_',
|
|
1560
1603
|
client: { PUBLIC_SITE_URL: z.url() },
|
|
1561
|
-
// No bare process.env:
|
|
1562
|
-
//
|
|
1604
|
+
// No bare process.env: import.meta.env carries the PUBLIC_ values, and the
|
|
1605
|
+
// file does not throw in the browser if a client component imports it.
|
|
1563
1606
|
runtimeEnv: { ...(typeof process === 'undefined' ? {} : process.env), ...import.meta.env },
|
|
1564
1607
|
emptyStringAsUndefined: true,
|
|
1565
1608
|
skipValidation: typeof process !== 'undefined' && !!process.env.SKIP_ENV_VALIDATION,
|
|
@@ -1573,6 +1616,12 @@ a browser-readable one MUST start with PUBLIC_ and is read from import.meta.env
|
|
|
1573
1616
|
(Vite; the engine registers PUBLIC_ beside VITE_ as a client prefix, nothing
|
|
1574
1617
|
to configure), which is why runtimeEnv merges both. Commit .env.example, not .env.
|
|
1575
1618
|
|
|
1619
|
+
CLIENT COMPONENTS DO NOT IMPORT src/env.ts. A "use client" file (or a lib file
|
|
1620
|
+
one imports) reads import.meta.env.PUBLIC_X directly, declared in
|
|
1621
|
+
ImportMetaEnv. Importing env there ships the validation library to the
|
|
1622
|
+
browser - measured: 20 kB gzipped of Zod + env-core in a chunk that needed one
|
|
1623
|
+
url. The value is still validated: the server imports env at startup.
|
|
1624
|
+
|
|
1576
1625
|
Next: NEXT_PUBLIC_* becomes PUBLIC_*; @t3-oss/env-nextjs becomes
|
|
1577
1626
|
@t3-oss/env-core with runtimeEnv as above (env-nextjs's experimental__runtimeEnv
|
|
1578
1627
|
is not needed).`,
|