@rsc-kit/mcp 0.14.0 → 0.16.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/answers.d.ts +7 -0
- package/dist/answers.js +28 -0
- package/dist/answers.js.map +1 -1
- package/dist/bundleGuides.d.ts +27 -0
- package/dist/bundleGuides.js +138 -0
- package/dist/bundleGuides.js.map +1 -0
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/recipes.js +138 -17
- package/dist/recipes.js.map +1 -1
- package/dist/report.d.ts +13 -0
- package/dist/report.js +1 -1
- package/dist/report.js.map +1 -1
- package/guides/api-routes.md +168 -0
- package/guides/authorization.md +288 -0
- package/guides/caching.md +57 -0
- package/guides/coming-from-next.md +151 -0
- package/guides/connection.md +98 -0
- package/guides/edge-caching.md +159 -0
- package/guides/errors.md +109 -0
- package/guides/file-uploads.md +119 -0
- package/guides/fonts.md +117 -0
- package/guides/forms.md +528 -0
- package/guides/getting-started.md +132 -0
- package/guides/images.md +83 -0
- package/guides/index.json +187 -0
- package/guides/installation.md +338 -0
- package/guides/introduction.md +119 -0
- package/guides/mcp.md +113 -0
- package/guides/metadata.md +289 -0
- package/guides/navigation.md +84 -0
- package/guides/no-javascript.md +76 -0
- package/guides/offline.md +215 -0
- package/guides/ppr.md +181 -0
- package/guides/pwa.md +260 -0
- package/guides/queries.md +340 -0
- package/guides/quick-start.md +99 -0
- package/guides/react-compiler.md +153 -0
- package/guides/redirects.md +143 -0
- package/guides/response-headers.md +66 -0
- package/guides/route-interception.md +206 -0
- package/guides/routing.md +458 -0
- package/guides/sections.md +74 -0
- package/guides/server-actions.md +444 -0
- package/guides/static-generation.md +347 -0
- package/guides/testing.md +158 -0
- package/guides/third-party-scripts.md +105 -0
- package/guides/typed-routes.md +139 -0
- package/guides/url-validation.md +143 -0
- package/guides/validation.md +175 -0
- package/guides/view-transitions.md +120 -0
- package/package.json +4 -3
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Queries
|
|
2
|
+
|
|
3
|
+
> Reading from the server over GET, and letting TanStack Query or SWR own everything above it.
|
|
4
|
+
|
|
5
|
+
`query()` marks a server function as a read, so the call goes out as a GET
|
|
6
|
+
instead of a POST. Your logs and rate limiters can then tell it apart from a
|
|
7
|
+
mutation, and a cache can hold the answer.
|
|
8
|
+
|
|
9
|
+
```ts title="src/listings.ts"
|
|
10
|
+
"use server"
|
|
11
|
+
|
|
12
|
+
import { query } from "@rsc-kit/core/query"
|
|
13
|
+
|
|
14
|
+
export const getListings = query(async (kind: string) => {
|
|
15
|
+
return db.listings.where({ kind })
|
|
16
|
+
})
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
That is the whole feature. There is no cache here, no batching and no
|
|
20
|
+
deduplication: [TanStack Query](https://tanstack.com/query) and
|
|
21
|
+
[SWR](https://swr.vercel.app) already do those and do them better. **This owns
|
|
22
|
+
the transport; they own everything above it.**
|
|
23
|
+
|
|
24
|
+
You still need `"use server"`, and it is not claiming your function mutates.
|
|
25
|
+
Despite the name it means *this may be called from the browser* — it is what
|
|
26
|
+
gives the function an id and the client a stub to call it through.
|
|
27
|
+
|
|
28
|
+
Two markers, two jobs. `"use server"` says the function crosses the boundary.
|
|
29
|
+
`query()` says it is a read.
|
|
30
|
+
|
|
31
|
+
:::caution[The call site picks the method]
|
|
32
|
+
`query()` permits the GET; it does not force it. Calling the function directly
|
|
33
|
+
from a client component is still a POST, because the browser cannot tell a query
|
|
34
|
+
id from an action id — only `fetchQuery` sends a GET.
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
getListings(kind) // POST, even though it is a query
|
|
38
|
+
fetchQuery(getListings, [kind]) // GET
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Development warns from the server when a query arrives at the action endpoint,
|
|
42
|
+
so a forgotten `fetchQuery` shows up rather than quietly costing you the method.
|
|
43
|
+
:::
|
|
44
|
+
|
|
45
|
+
## What it changes
|
|
46
|
+
|
|
47
|
+
Only the method. Same types, same arguments, same lack of an endpoint to write.
|
|
48
|
+
|
|
49
|
+
Three things follow from that:
|
|
50
|
+
|
|
51
|
+
- **Access logs and rate limiters** stop counting your reads as mutations. A
|
|
52
|
+
limiter that allows 10 writes a minute should not be spending them on a list
|
|
53
|
+
being refreshed.
|
|
54
|
+
- **A GET is the only shape a cache can keep.** A browser cache, a CDN or a
|
|
55
|
+
service worker can hold one; a POST can never be held by any of them.
|
|
56
|
+
- **Repeating it is safe by contract**, which is what a prefetcher, a crawler
|
|
57
|
+
or a retrying proxy assumes when it sees a GET.
|
|
58
|
+
|
|
59
|
+
TanStack Start makes the same choice from the other side: its `createServerFn()`
|
|
60
|
+
is a GET unless you ask for `{ method: 'POST' }`. We cannot default it that way
|
|
61
|
+
— React gives us one directive for everything, so a GET default would send
|
|
62
|
+
mutations as GET too.
|
|
63
|
+
|
|
64
|
+
### If you skip it
|
|
65
|
+
|
|
66
|
+
A plain server action still works as a cache library's fetcher, and nothing
|
|
67
|
+
breaks:
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
useQuery({ queryKey: ["listings", kind], queryFn: () => getListings(kind) })
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
You lose the three things above and keep everything else. So this is a
|
|
74
|
+
reasonable place to start, and `query()` is the thing to reach for once a read
|
|
75
|
+
is worth being precise about — anything hot enough to show up in a rate limiter,
|
|
76
|
+
or public enough to be worth caching.
|
|
77
|
+
|
|
78
|
+
One caveat on the second: data that is the same for everyone usually wants
|
|
79
|
+
[prerendering](/guides/static-generation/) rather than a client read at all, and
|
|
80
|
+
a personal read answers `no-store` and is not cached either way. The caching win
|
|
81
|
+
is real, and narrower than it sounds.
|
|
82
|
+
|
|
83
|
+
## Reading it
|
|
84
|
+
|
|
85
|
+
**In a server component, call it.** It is ordinary server code — no HTTP, no
|
|
86
|
+
cache, no client involved:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
const listings = await getListings("stay")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Better still, do not await it.** Pass the promise down and let a client
|
|
93
|
+
component resolve it, and the data streams with the page:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
export default function Page() {
|
|
97
|
+
const listings = getListings("stay")
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<Suspense fallback={<Skeleton />}>
|
|
101
|
+
<List listings={listings} /> {/* "use client": use(listings) */}
|
|
102
|
+
</Suspense>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
React serialises the promise as a pending row in the payload, so the shell
|
|
108
|
+
paints at once and the rows arrive when the query answers — in the same
|
|
109
|
+
response, with no request from the browser. Nothing in this package is involved;
|
|
110
|
+
`use()` is React's. Reach for this first.
|
|
111
|
+
|
|
112
|
+
**When the browser decides what to read** — a filter, another page, a refresh —
|
|
113
|
+
hand `fetchQuery` to your cache library:
|
|
114
|
+
|
|
115
|
+
```tsx
|
|
116
|
+
import { fetchQuery } from "@rsc-kit/core/queryClient"
|
|
117
|
+
|
|
118
|
+
// TanStack Query
|
|
119
|
+
useQuery({
|
|
120
|
+
queryKey: ["listings", kind],
|
|
121
|
+
queryFn: () => fetchQuery(getListings, [kind]),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// SWR
|
|
125
|
+
useSWR(["listings", kind], () => fetchQuery(getListings, [kind]))
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`fetchQuery` goes to the server every time. That is what a fetcher needs:
|
|
129
|
+
staleness, revalidation and deduplication belong to the library holding the
|
|
130
|
+
answer, not to the thing that fetches it.
|
|
131
|
+
|
|
132
|
+
:::caution[Keep the arrow]
|
|
133
|
+
TanStack calls a bare `queryFn` with its own context — `{ client, queryKey,
|
|
134
|
+
meta, signal }` — and a server function serialises whatever it is handed, so
|
|
135
|
+
passing one unwrapped would try to put an `AbortSignal` on the wire. The arrow
|
|
136
|
+
is where you choose what travels.
|
|
137
|
+
:::
|
|
138
|
+
|
|
139
|
+
### Paging, polling, and the rest
|
|
140
|
+
|
|
141
|
+
All of it belongs to the library, and all of it works:
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
// Infinite. The cursor is just an argument.
|
|
145
|
+
useInfiniteQuery({
|
|
146
|
+
queryKey: ["feed"],
|
|
147
|
+
queryFn: ({ pageParam }) => fetchQuery(getFeed, [pageParam]),
|
|
148
|
+
initialPageParam: null,
|
|
149
|
+
getNextPageParam: (last) => last.nextCursor,
|
|
150
|
+
initialData: { pages: [first], pageParams: [null] },
|
|
151
|
+
staleTime: 60_000,
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Data that changes while you watch. No live connection needed.
|
|
155
|
+
useQuery({ queryKey: ["seats"], queryFn: () => fetchQuery(getSeats, []), refetchInterval: 2_000 })
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Page one comes from a server component and costs no request; later pages are
|
|
159
|
+
ordinary reads. `/infinite`, `/pagination` and `/polling` in the example app are
|
|
160
|
+
these three patterns end to end.
|
|
161
|
+
|
|
162
|
+
:::caution[A seed is stale by default]
|
|
163
|
+
TanStack treats `initialData` as stale at its default `staleTime` of `0`, so
|
|
164
|
+
without an explicit `staleTime` it refetches page one on mount and the round
|
|
165
|
+
trip you seeded to avoid happens anyway. SWR's `fallbackData` has the same
|
|
166
|
+
shape of caveat, and `refetchInterval` pauses while the tab is hidden.
|
|
167
|
+
:::
|
|
168
|
+
|
|
169
|
+
### Where it will not work
|
|
170
|
+
|
|
171
|
+
`fetchQuery` only works in the browser. React refuses a server-function call
|
|
172
|
+
during the first render, so a component that also renders on the server must not
|
|
173
|
+
call it there.
|
|
174
|
+
|
|
175
|
+
You will not hit this through TanStack or SWR — they fetch in an effect, after
|
|
176
|
+
hydration.
|
|
177
|
+
|
|
178
|
+
## Sharing a check across reads
|
|
179
|
+
|
|
180
|
+
A query has no middleware above it, so each one checks for itself. If that means
|
|
181
|
+
writing the same check in every file, put it on the client you already use for
|
|
182
|
+
actions:
|
|
183
|
+
|
|
184
|
+
```ts title="src/server/client.ts"
|
|
185
|
+
'use server'
|
|
186
|
+
|
|
187
|
+
import { createActionClient } from '@rsc-kit/core/action'
|
|
188
|
+
|
|
189
|
+
export const client = createActionClient({ onError: report })
|
|
190
|
+
.use(async ({ next }) => {
|
|
191
|
+
const user = await currentUser()
|
|
192
|
+
|
|
193
|
+
if (!user) throw new Error('Not signed in')
|
|
194
|
+
|
|
195
|
+
return next({ ctx: { user } })
|
|
196
|
+
})
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
```ts title="src/server/posts.ts"
|
|
200
|
+
'use server'
|
|
201
|
+
|
|
202
|
+
import { client } from './client'
|
|
203
|
+
|
|
204
|
+
export const createPost = client.input(schema).handler(async ({ input, ctx }) => …) // POST
|
|
205
|
+
export const getPosts = client.input(filter).query(async ({ input, ctx }) => …) // GET
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
One client, one set of middleware, one `onError`. `.handler()` makes an action;
|
|
209
|
+
`.query()` makes a query. Neither can be added without the check.
|
|
210
|
+
|
|
211
|
+
### They fail differently, on purpose
|
|
212
|
+
|
|
213
|
+
An action **returns** its failures, because React strips a thrown message in
|
|
214
|
+
production. A query **throws** — every cache library reports failure by
|
|
215
|
+
rejection, and one that answered with an error-shaped object would look like a
|
|
216
|
+
successful read of something odd.
|
|
217
|
+
|
|
218
|
+
So a refused read rejects with a real `Error`. A validation failure keeps its
|
|
219
|
+
fields:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
const { error } = useQuery({ queryKey: ['posts'], queryFn: () => fetchQuery(getPosts, [filter]) })
|
|
223
|
+
|
|
224
|
+
error.message // 'Validation failed'
|
|
225
|
+
error.errors // { title: ['too short'] }
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Securing a query
|
|
229
|
+
|
|
230
|
+
A query is a public GET endpoint. Treat it as one.
|
|
231
|
+
|
|
232
|
+
### Route middleware does not run
|
|
233
|
+
|
|
234
|
+
`middleware.ts` guards a **route**, and a query has no route — it is reachable
|
|
235
|
+
whoever is asking and whatever page they came from. A query behind a guarded
|
|
236
|
+
page is not guarded.
|
|
237
|
+
|
|
238
|
+
What you do get is the request: the visitor's cookies and session are bound for
|
|
239
|
+
the call, so a query can check for itself.
|
|
240
|
+
|
|
241
|
+
One query is fine written by hand:
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
export const getOrders = query(async () => {
|
|
245
|
+
const user = await currentUser()
|
|
246
|
+
|
|
247
|
+
if (!user) throw new Error("Not signed in")
|
|
248
|
+
|
|
249
|
+
return db.orders.forUser(user.id)
|
|
250
|
+
})
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Twenty is twenty chances to forget, and the one you forget is the one that
|
|
254
|
+
matters. Put the check on the client you already use for actions and build
|
|
255
|
+
every read from it:
|
|
256
|
+
|
|
257
|
+
```ts title="src/server/orders.ts"
|
|
258
|
+
'use server'
|
|
259
|
+
|
|
260
|
+
import { client } from './client'
|
|
261
|
+
|
|
262
|
+
export const getOrders = client.query(async ({ ctx }) => db.orders.forUser(ctx.user.id))
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
`ctx.user` is typed and non-null because the only way into the handler was
|
|
266
|
+
through the middleware that put it there — so a query **cannot be added without
|
|
267
|
+
the check**. Same client, same middleware, same `onError` as your actions;
|
|
268
|
+
`.handler()` makes a mutation and `.query()` makes a read. See
|
|
269
|
+
[sharing a check across reads](#sharing-a-check-across-reads) below.
|
|
270
|
+
|
|
271
|
+
Authorise on **identity, not arguments**. `getOrder(id)` that trusts the id is
|
|
272
|
+
the whole of an IDOR: the caller chooses the id.
|
|
273
|
+
|
|
274
|
+
### Only `query()` is reachable
|
|
275
|
+
|
|
276
|
+
The endpoint refuses anything that is not a query, which is what keeps every
|
|
277
|
+
action you have registered off a GET url. So export the wrapped value, not the
|
|
278
|
+
bare function beside it.
|
|
279
|
+
|
|
280
|
+
An unknown id and a real-but-unmarked one get the same 404. Telling them apart
|
|
281
|
+
would let someone probe for your action ids.
|
|
282
|
+
|
|
283
|
+
### A query must never write
|
|
284
|
+
|
|
285
|
+
It is a GET. A prefetcher, a crawler or a retry will repeat it.
|
|
286
|
+
|
|
287
|
+
### What the endpoint does for you
|
|
288
|
+
|
|
289
|
+
- **Requires `X-RSC-Query`.** A GET with no unusual header is a *simple*
|
|
290
|
+
request, so any page anywhere could trigger one with
|
|
291
|
+
`<img src="…/_rsc/query?…">` and it would carry the visitor's cookies — CORS
|
|
292
|
+
stops them reading the answer, not the read running. That header is not
|
|
293
|
+
CORS-safelisted, so a browser preflights it and nothing here answers a
|
|
294
|
+
preflight. It is the same protection a POST carrying `X-RSC-Action` had.
|
|
295
|
+
- Refuses a cross-origin request when an `Origin` is present.
|
|
296
|
+
- Refuses an oversized url with `414` before decoding anything.
|
|
297
|
+
- Sends `Vary: Cookie`, so a cacheable answer is never shared between visitors.
|
|
298
|
+
|
|
299
|
+
None of that authorises anything. The query does that.
|
|
300
|
+
|
|
301
|
+
### Arguments are public
|
|
302
|
+
|
|
303
|
+
They travel in the url, so they reach access logs, browser history and referrer
|
|
304
|
+
headers. Never take a token or a password reset code as a query argument — that
|
|
305
|
+
is an action.
|
|
306
|
+
|
|
307
|
+
Arguments too large for a url, or containing a `File`, make the read fall back
|
|
308
|
+
to a POST rather than failing. It still works; it simply stops being cacheable,
|
|
309
|
+
and development warns when it happens.
|
|
310
|
+
|
|
311
|
+
## Caching
|
|
312
|
+
|
|
313
|
+
Answers default to `private, no-store`. A query may read the session, and a
|
|
314
|
+
cacheable answer to a personal read is how one visitor is served another's data.
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
export const getPricing = query(async () => tiers(), { cache: "public", maxAge: 300 })
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### You probably do not need to change it
|
|
321
|
+
|
|
322
|
+
There are two caches and they solve different problems.
|
|
323
|
+
|
|
324
|
+
**Your cache library's**, in memory and per tab, decides whether to ask again.
|
|
325
|
+
It needs nothing from this option — `no-store` does not stop TanStack or SWR
|
|
326
|
+
holding an answer, because they are not an HTTP cache.
|
|
327
|
+
|
|
328
|
+
**The HTTP one**, which `cache` controls, decides whether an answer survives a
|
|
329
|
+
reload, works offline, or can be shared by a CDN.
|
|
330
|
+
|
|
331
|
+
Most apps want only the first. And note that data which is the same for everyone
|
|
332
|
+
usually wants [prerendering](/guides/static-generation/) rather than a client
|
|
333
|
+
read at all — which is a better answer than any cache. For a personal read you
|
|
334
|
+
want across reloads, persist your cache library rather than widening the read.
|
|
335
|
+
|
|
336
|
+
## During a build
|
|
337
|
+
|
|
338
|
+
A query is an ordinary function, so it follows the ordinary rule. One reading a
|
|
339
|
+
database the build machine can reach runs at build time and the page is frozen
|
|
340
|
+
with real data in it. Call `connection()` inside the query to opt out.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Quick start
|
|
2
|
+
|
|
3
|
+
> A running app in one command, or added to a project you already have.
|
|
4
|
+
|
|
5
|
+
## A new app
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun create rsc-kit@latest my-app
|
|
9
|
+
cd my-app
|
|
10
|
+
bun run dev
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
That is it. You get a page, a layout, a client component and a Vite config,
|
|
14
|
+
wired together and running. There is no server file — Nitro builds one from the
|
|
15
|
+
preset in that config when you build.
|
|
16
|
+
|
|
17
|
+
When you are ready to ship:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bun run build
|
|
21
|
+
bun run start
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The build prints what it did:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
○ /
|
|
28
|
+
|
|
29
|
+
○ (Static) prerendered as static content
|
|
30
|
+
|
|
31
|
+
1 static
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Pages marked `○` are **static**: rendered once at build time rather than for
|
|
35
|
+
each visitor.
|
|
36
|
+
|
|
37
|
+
## An app you already have
|
|
38
|
+
|
|
39
|
+
Already have a project? Add rsc-kit to it:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
bunx rsc-kit@latest init
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
It reads your `package.json`, works out which server you use and where your
|
|
46
|
+
source lives, and writes only what is missing:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
server hono
|
|
50
|
+
source src
|
|
51
|
+
react will be added
|
|
52
|
+
|
|
53
|
+
+ src/app/layout.tsx
|
|
54
|
+
+ src/app/page.tsx
|
|
55
|
+
+ vite.config.ts
|
|
56
|
+
~ package.json — added @rsc-kit/core, react, react-dom, vite, nitro, …
|
|
57
|
+
~ scripts — dev, build, start, compile, typecheck
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
No server file. Nitro builds one around the route tree, so the app owns a route
|
|
61
|
+
tree and a vite config and nothing in between.
|
|
62
|
+
|
|
63
|
+
**It never overwrites anything.** If you already have a `vite.config.ts`, it
|
|
64
|
+
prints the edit to make instead of replacing work you have done. Running it twice is safe — the second run just tells you what is already
|
|
65
|
+
in place.
|
|
66
|
+
|
|
67
|
+
Then:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
bun install
|
|
71
|
+
bun run dev
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Add a page
|
|
75
|
+
|
|
76
|
+
Routes are directories under `src/app`. Create a folder, put a `page.tsx` in
|
|
77
|
+
it, and it exists:
|
|
78
|
+
|
|
79
|
+
```tsx title="src/app/about/page.tsx"
|
|
80
|
+
export default function AboutPage() {
|
|
81
|
+
return <h1>About</h1>
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```sh
|
|
86
|
+
bun run build # the route tree is read at build time
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Doing it by hand
|
|
90
|
+
|
|
91
|
+
If you would rather wire it up yourself — or the CLI cannot reach your
|
|
92
|
+
setup — [Installation](/installation) walks through the same result one file at
|
|
93
|
+
a time.
|
|
94
|
+
|
|
95
|
+
## Where next
|
|
96
|
+
|
|
97
|
+
- [Routing](/guides/routing) — layouts, dynamic segments, parallel slots
|
|
98
|
+
- [Server actions](/guides/server-actions) — mutations without an API route
|
|
99
|
+
- [Where it runs](/hosts/where-it-runs) — presets, and compiling to a binary
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# React Compiler
|
|
2
|
+
|
|
3
|
+
> Enabling the compiler in the build.
|
|
4
|
+
|
|
5
|
+
The React Compiler memoises client components for you, so `useMemo`,
|
|
6
|
+
`useCallback` and `React.memo` mostly stop being things you write.
|
|
7
|
+
|
|
8
|
+
Nothing here special-cases it. The build runs your project's own Vite config,
|
|
9
|
+
so the compiler is enabled the way it is in any Vite app — by adding
|
|
10
|
+
`@vitejs/plugin-react` after `rscKit()` and turning it on there.
|
|
11
|
+
|
|
12
|
+
<Aside type="note" title="Order matters">
|
|
13
|
+
`rscKit()` includes `@vitejs/plugin-rsc`, which has to see modules before
|
|
14
|
+
any React layer transforms them. The React plugin goes **after** it.
|
|
15
|
+
</Aside>
|
|
16
|
+
|
|
17
|
+
## Two ways to run it
|
|
18
|
+
|
|
19
|
+
The compiler has a native implementation and a Babel one. Both produce the same
|
|
20
|
+
transform; they differ in what they cost to run and how settled they are.
|
|
21
|
+
|
|
22
|
+
### Native, through oxc
|
|
23
|
+
|
|
24
|
+
The faster path, and the least to install. `compiler: true` is
|
|
25
|
+
[experimental](https://react.dev/learn/react-compiler/installation) and needs
|
|
26
|
+
`oxc-transform-react` present — the plugin looks for it by name:
|
|
27
|
+
|
|
28
|
+
<PackageManagers pkg="@vitejs/plugin-react oxc-transform-react" dev />
|
|
29
|
+
|
|
30
|
+
```ts title="vite.config.ts"
|
|
31
|
+
import { defineConfig } from 'vite';
|
|
32
|
+
import react from '@vitejs/plugin-react';
|
|
33
|
+
import { rscKit } from '@rsc-kit/core/vite';
|
|
34
|
+
|
|
35
|
+
export default defineConfig({
|
|
36
|
+
plugins: [
|
|
37
|
+
rscKit({ sourceDir: 'src' }),
|
|
38
|
+
react({ compiler: true }),
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Pass an object instead of `true` to configure it.
|
|
44
|
+
|
|
45
|
+
### Babel
|
|
46
|
+
|
|
47
|
+
The reference implementation. In `@vitejs/plugin-react` 6 the inline `babel`
|
|
48
|
+
option was removed, so the preset is applied through `@rolldown/plugin-babel`:
|
|
49
|
+
|
|
50
|
+
<PackageManagers pkg="@vitejs/plugin-react @rolldown/plugin-babel babel-plugin-react-compiler" dev />
|
|
51
|
+
|
|
52
|
+
```ts title="vite.config.ts"
|
|
53
|
+
import { defineConfig } from 'vite';
|
|
54
|
+
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
|
|
55
|
+
import babel from '@rolldown/plugin-babel';
|
|
56
|
+
import { rscKit } from '@rsc-kit/core/vite';
|
|
57
|
+
|
|
58
|
+
export default defineConfig({
|
|
59
|
+
plugins: [
|
|
60
|
+
rscKit({ sourceDir: 'src' }),
|
|
61
|
+
react(),
|
|
62
|
+
babel({ presets: [reactCompilerPreset()] }),
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
On `@vitejs/plugin-react` 5 and earlier, the inline option still exists:
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
react({
|
|
71
|
+
babel: { plugins: ['babel-plugin-react-compiler'] },
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Checks worth running first
|
|
76
|
+
|
|
77
|
+
The compiler only memoises components it can prove are safe to memoise, and it
|
|
78
|
+
skips the rest silently. These are how you find out which is which, and they
|
|
79
|
+
are worth running **before** you turn it on rather than after.
|
|
80
|
+
|
|
81
|
+
**Type checking.** The compiler assumes your code means what its types say. Run
|
|
82
|
+
`tsc --noEmit` and fix what it reports first — an untyped `any` threading
|
|
83
|
+
through a component is exactly the shape the compiler has to give up on.
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
tsc --noEmit
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**The health check.** Reports how many components in your codebase the compiler
|
|
90
|
+
can handle, and why the others are refused:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx react-compiler-healthcheck
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**The lint rule.** `eslint-plugin-react-hooks` includes the compiler's own
|
|
97
|
+
diagnostics — the Rules of React violations that make a component
|
|
98
|
+
uncompilable — so they surface as you write rather than as silence in the
|
|
99
|
+
build:
|
|
100
|
+
|
|
101
|
+
```js title="eslint.config.js"
|
|
102
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
103
|
+
|
|
104
|
+
export default [reactHooks.configs.recommended];
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**StrictMode.** The compiler's assumptions are the Rules of React, and
|
|
108
|
+
StrictMode is what surfaces breaking them at runtime — double-invoked renders
|
|
109
|
+
catch the impure ones.
|
|
110
|
+
|
|
111
|
+
<Aside type="tip" title="It applies to client components">
|
|
112
|
+
Server components render once and are thrown away, so there is nothing to
|
|
113
|
+
memoise in them. The compiler earns its keep in the `"use client"` half of
|
|
114
|
+
the app.
|
|
115
|
+
</Aside>
|
|
116
|
+
|
|
117
|
+
## Confirming it ran
|
|
118
|
+
|
|
119
|
+
The compiler leaves a cache array at the top of every component it compiled.
|
|
120
|
+
Build without minification and look for it:
|
|
121
|
+
|
|
122
|
+
```js
|
|
123
|
+
function Counter() {
|
|
124
|
+
const $ = _c(10); // ← compiled
|
|
125
|
+
const [count, setCount] = useState(0);
|
|
126
|
+
|
|
127
|
+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
|
128
|
+
// …
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
No `_c(...)` and no `memo_cache_sentinel` means that component was skipped —
|
|
134
|
+
which the health check will explain.
|
|
135
|
+
|
|
136
|
+
## Opting a component out
|
|
137
|
+
|
|
138
|
+
```tsx
|
|
139
|
+
"use client";
|
|
140
|
+
|
|
141
|
+
export default function LegacyWidget() {
|
|
142
|
+
"use no memo";
|
|
143
|
+
|
|
144
|
+
return <div>…</div>;
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Turning it off
|
|
149
|
+
|
|
150
|
+
Drop `compiler: true`, or remove the plugin. Neither the router nor the build
|
|
151
|
+
depends on it being there.
|
|
152
|
+
|
|
153
|
+
Further reading: [React Compiler installation](https://react.dev/learn/react-compiler/installation).
|