@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,458 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
> File-based routes, layouts, loading states and parallel slots.
|
|
4
|
+
|
|
5
|
+
Routes are directories. The plugin reads your source tree at build time and
|
|
6
|
+
generates the entry that knows about them — there is nothing to register, and
|
|
7
|
+
no route table to keep in sync with the files.
|
|
8
|
+
|
|
9
|
+
## File-based routing
|
|
10
|
+
|
|
11
|
+
Pages and layouts follow the Next.js App Router conventions. The default source
|
|
12
|
+
directory is `src/app`:
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
src/app/
|
|
16
|
+
layout.tsx root layout — wraps every page
|
|
17
|
+
loading.tsx Suspense fallback for every page
|
|
18
|
+
page.tsx GET /
|
|
19
|
+
@sidebar/
|
|
20
|
+
page.tsx parallel slot "sidebar"
|
|
21
|
+
about/
|
|
22
|
+
page.tsx GET /about
|
|
23
|
+
docs/
|
|
24
|
+
layout.tsx nested layout for /docs/*
|
|
25
|
+
page.tsx GET /docs
|
|
26
|
+
sidebar.tsx colocated component — NOT a route
|
|
27
|
+
[slug]/
|
|
28
|
+
page.tsx GET /docs/:slug
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
<Aside type="note" title="Somewhere else?">
|
|
32
|
+
`src/app` is the default. Set `sourceDir` in the plugin options to put it
|
|
33
|
+
elsewhere — everything below works the same, only the prefix differs.
|
|
34
|
+
</Aside>
|
|
35
|
+
|
|
36
|
+
### Special files
|
|
37
|
+
|
|
38
|
+
- `page.tsx` — defines a route. Only files named `page.*` create routes.
|
|
39
|
+
- `layout.tsx` — wraps every page in the same directory and below. Receives `children`.
|
|
40
|
+
- `loading.tsx` — Suspense fallback. Wraps the page in `<Suspense>` automatically. Hierarchical: the nearest one to the page wins.
|
|
41
|
+
- `default.tsx` — what a parallel slot renders when no page matches it.
|
|
42
|
+
- `middleware.ts` — runs before anything at or below the directory renders, on every path. Not a component: it returns nothing and decides, by redirecting or throwing. Composes up the tree like layouts. See [Authorization](/guides/authorization).
|
|
43
|
+
- `@folder/` — a parallel route slot. Rendered as a named prop on the layout that declares it. Adds no URL segment.
|
|
44
|
+
|
|
45
|
+
Everything else in the directory is a colocated component — importable by pages
|
|
46
|
+
and layouts, but not a route.
|
|
47
|
+
|
|
48
|
+
### Dynamic segments
|
|
49
|
+
|
|
50
|
+
Square brackets mark a dynamic segment. `[slug]` matches one segment and
|
|
51
|
+
arrives as a `slug` prop on the page; `[...path]` catches the rest of the URL.
|
|
52
|
+
|
|
53
|
+
```tsx title="src/app/docs/[slug]/page.tsx"
|
|
54
|
+
export default async function DocsPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
55
|
+
const { slug } = await params;
|
|
56
|
+
|
|
57
|
+
return <h1>{slug}</h1>;
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`params` is a promise, and that is deliberate. Awaiting it lets everything above
|
|
62
|
+
the read paint first, and the build stores that as one shell serving every url
|
|
63
|
+
the route matches. Reading it synchronously would force a fresh render for every
|
|
64
|
+
visitor. `searchParams` works the same way.
|
|
65
|
+
|
|
66
|
+
### Route groups
|
|
67
|
+
|
|
68
|
+
Parenthesised directories like `(marketing)` group files without adding a URL
|
|
69
|
+
segment. `app/(marketing)/pricing/page.tsx` serves `GET /pricing`.
|
|
70
|
+
|
|
71
|
+
## Fetching data
|
|
72
|
+
|
|
73
|
+
A server component runs on the server, so it reads its data the way any
|
|
74
|
+
server-side module does — by importing it:
|
|
75
|
+
|
|
76
|
+
```tsx title="src/app/dashboard/page.tsx"
|
|
77
|
+
import { revenue, orderCount } from '../../db/stats';
|
|
78
|
+
|
|
79
|
+
export default async function DashboardPage() {
|
|
80
|
+
const [amount, orders] = await Promise.all([revenue(), orderCount()]);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div>
|
|
84
|
+
<h1>Dashboard</h1>
|
|
85
|
+
<p>Revenue: {amount}</p>
|
|
86
|
+
<p>Orders: {orders}</p>
|
|
87
|
+
</div>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
There is no data-fetching API to learn, and no request context to thread
|
|
93
|
+
through. `db/stats` never enters the client module graph, so neither does the
|
|
94
|
+
connection string it opens.
|
|
95
|
+
|
|
96
|
+
## Server components
|
|
97
|
+
|
|
98
|
+
Server components are the default: any `.tsx` file without a `"use client"`
|
|
99
|
+
directive is one. They may be async, and they may render client components.
|
|
100
|
+
|
|
101
|
+
## Client components
|
|
102
|
+
|
|
103
|
+
`"use client"` on the first line marks a module as client code. It hydrates in
|
|
104
|
+
the browser and can use hooks, event handlers and browser APIs.
|
|
105
|
+
|
|
106
|
+
```tsx title="src/components/Counter.tsx"
|
|
107
|
+
"use client";
|
|
108
|
+
|
|
109
|
+
import { useState } from 'react';
|
|
110
|
+
|
|
111
|
+
export default function Counter() {
|
|
112
|
+
const [count, setCount] = useState(0);
|
|
113
|
+
|
|
114
|
+
return <button onClick={() => setCount(count + 1)}>Count: {count}</button>;
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
The build turns each one into a client reference, so a server component can
|
|
119
|
+
import and render it without shipping its own source.
|
|
120
|
+
|
|
121
|
+
## Navigation
|
|
122
|
+
|
|
123
|
+
`Link` intercepts the click, fetches the payload, and updates the React tree
|
|
124
|
+
without a reload.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import Link from '@rsc-kit/core/Link';
|
|
128
|
+
|
|
129
|
+
<Link href="/docs/installation">Docs</Link>
|
|
130
|
+
|
|
131
|
+
// Prefetch on hover (the default)
|
|
132
|
+
<Link href="/settings" prefetch="hover">Settings</Link>
|
|
133
|
+
|
|
134
|
+
// Prefetch as soon as the link mounts
|
|
135
|
+
<Link href="/dashboard" prefetch="mount">Dashboard</Link>
|
|
136
|
+
|
|
137
|
+
// Keep the scroll position instead of jumping to the top
|
|
138
|
+
<Link href="/docs/rsc" preserveScroll>Stay here</Link>
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
A navigation sends the layout chain the client already has mounted, and the
|
|
142
|
+
server answers with only the part below the deepest layout they share. The
|
|
143
|
+
layouts above it are never re-rendered, which is why the nav does not flicker
|
|
144
|
+
and why a half-typed form in a retained page survives.
|
|
145
|
+
|
|
146
|
+
### Programmatic navigation
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
"use client";
|
|
150
|
+
import { visit, prefetch } from '@rsc-kit/core/router';
|
|
151
|
+
|
|
152
|
+
await visit('/about');
|
|
153
|
+
await visit('/dashboard', { replace: true }); // no back-button entry
|
|
154
|
+
prefetch('/settings'); // warm the cache
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
A default export is available too, if you prefer `router.visit('/about')`.
|
|
158
|
+
|
|
159
|
+
### Link props
|
|
160
|
+
|
|
161
|
+
| Prop | Type | Default | What it does |
|
|
162
|
+
| --- | --- | --- | --- |
|
|
163
|
+
| `href` | `string` | — | The destination. An off-origin URL is left to the browser. |
|
|
164
|
+
| `prefetch` | `"hover" \| "mount" \| "click" \| "none"` | `"hover"` | When to warm the cache. `hover` waits 100 ms for the pointer to settle, so crossing a nav bar does not fire a request per link. `true`/`false` are accepted as aliases for `hover`/`none`. |
|
|
165
|
+
| `cacheFor` | `number` | `30000` | How long a prefetched payload stays usable, in milliseconds. |
|
|
166
|
+
| `replace` | `boolean` | `false` | Replace the history entry instead of pushing one. |
|
|
167
|
+
| `preserveScroll` | `boolean` | `false` | Stay where you are instead of scrolling to the top. |
|
|
168
|
+
|
|
169
|
+
Anything else is passed straight to the underlying `<a>`, `ref` included.
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
### Loading state
|
|
173
|
+
|
|
174
|
+
Each `Link` carries a `data-pending` attribute while its navigation is in
|
|
175
|
+
flight, which is usually enough on its own:
|
|
176
|
+
|
|
177
|
+
```css
|
|
178
|
+
a[data-pending] {
|
|
179
|
+
opacity: 0.5;
|
|
180
|
+
pointer-events: none;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
When the affected UI is not the link itself, `useLinkStatus` reads the nearest
|
|
185
|
+
parent `Link`'s state:
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
"use client";
|
|
189
|
+
|
|
190
|
+
import { useLinkStatus } from '@rsc-kit/core/useLinkStatus';
|
|
191
|
+
|
|
192
|
+
function NavContent({ children }: { children: React.ReactNode }) {
|
|
193
|
+
const { pending } = useLinkStatus();
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<span className={pending ? 'opacity-50' : 'opacity-100'}>
|
|
197
|
+
{children}
|
|
198
|
+
{pending && <span className="spinner" />}
|
|
199
|
+
</span>
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Typed links
|
|
205
|
+
|
|
206
|
+
Every build writes the routes it found into `.rsc-kit/rsc-routes.d.ts`, so
|
|
207
|
+
`href` is checked against them rather than being any old string:
|
|
208
|
+
|
|
209
|
+
```tsx
|
|
210
|
+
<Link href="/orders">Orders</Link>
|
|
211
|
+
<Link href="/ordres">Orders</Link>
|
|
212
|
+
// ^ Type '"/ordres"' is not assignable. Did you mean '"/orders"'?
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
For a route with params, write the url — a template literal is checked the same
|
|
216
|
+
way, including the static part around the value:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
<Link href={`/posts/${post.slug}`}>Read</Link>
|
|
220
|
+
<Link href={`/postz/${post.slug}`}>Read</Link>
|
|
221
|
+
// ^ Type '`/postz/${string}`' is not assignable
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The same type covers `redirect()`, `visit()`, `prefetch()` and `Form`'s
|
|
225
|
+
`action`. Nothing to
|
|
226
|
+
configure and no flag — the file is regenerated on every build, so deleting a
|
|
227
|
+
page makes every link to it stop compiling.
|
|
228
|
+
|
|
229
|
+
One thing you will hit immediately: a list of links widens to `string` unless
|
|
230
|
+
you say otherwise. `satisfies` keeps each literal *and* checks it, so the typo
|
|
231
|
+
fails at the list rather than at the `Link` that renders it:
|
|
232
|
+
|
|
233
|
+
```tsx title="src/components/Nav.tsx"
|
|
234
|
+
// `satisfies` rather than a type annotation: an annotation would widen href to
|
|
235
|
+
// Href and lose which one each entry is, while this keeps the literals and
|
|
236
|
+
// still checks them — so a typo fails here, at the list, rather than at the
|
|
237
|
+
// Link that renders it.
|
|
238
|
+
const links = [
|
|
239
|
+
{ href: '/', label: 'Home' },
|
|
240
|
+
{ href: '/dashboard', label: 'Dashboard' },
|
|
241
|
+
{ href: '/posts/hello-world', label: 'A Post' },
|
|
242
|
+
] satisfies { href: Href; label: string }[]
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### When the value is not already a url
|
|
246
|
+
|
|
247
|
+
A template literal interpolates whatever it is given, and the type cannot stop
|
|
248
|
+
it — `${string}` covers spaces, slashes and `?` as happily as a slug. So a
|
|
249
|
+
title used as an identifier quietly produces a url that means something else:
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
`/posts/${'Q3 report / draft?v=2'}`
|
|
253
|
+
// '/posts/Q3 report / draft?v=2' — three segments and a query string
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Encode it in the template, the same as anywhere else. There is no url builder
|
|
257
|
+
to reach for and nothing new to remember:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
<Link href={`/posts/${encodeURIComponent(post.title)}`}>Read</Link>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
<Aside type="note" title="Why there is no route() helper">
|
|
264
|
+
The url *is* the file path, and a template literal is already checked against
|
|
265
|
+
it. A builder would only give you a second way to write the same line.
|
|
266
|
+
</Aside>
|
|
267
|
+
|
|
268
|
+
When a destination is computed rather than written, cast it — that is the seam
|
|
269
|
+
where you are telling the typechecker something it cannot know:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import type { Href } from '@rsc-kit/core/routes';
|
|
273
|
+
|
|
274
|
+
<Link href={savedPath as Href}>Resume</Link>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Two limits. A dynamic segment widens to `${string}`, and a template literal type
|
|
278
|
+
cannot say "no slashes here" — so `/posts/a/b` type-checks against
|
|
279
|
+
`/posts/[slug]`. And before your first build there are no route types yet, so
|
|
280
|
+
every href falls back to `string`.
|
|
281
|
+
|
|
282
|
+
## Reading the url from a client component
|
|
283
|
+
|
|
284
|
+
`usePathname` and `useSearchParams` read the current url and follow
|
|
285
|
+
navigations:
|
|
286
|
+
|
|
287
|
+
```tsx title="src/components/Filters.tsx"
|
|
288
|
+
"use client";
|
|
289
|
+
|
|
290
|
+
import { useSearchParams } from '@rsc-kit/core/useSearchParams';
|
|
291
|
+
|
|
292
|
+
export function Filters() {
|
|
293
|
+
const q = useSearchParams().get('q');
|
|
294
|
+
|
|
295
|
+
return <p>Showing results for {q}</p>;
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
They behave differently on the server, for a reason. A **pathname** is fixed for
|
|
300
|
+
a stored page, so `usePathname` just answers with the url being rendered.
|
|
301
|
+
|
|
302
|
+
A **query string** is not — the same route is asked for with `?q=shoes` and
|
|
303
|
+
`?q=hats` — so there is no honest answer at build time, and `useSearchParams`
|
|
304
|
+
throws rather than pretending it is empty.
|
|
305
|
+
|
|
306
|
+
Wrap it, and the throw becomes the fallback:
|
|
307
|
+
|
|
308
|
+
```tsx title="src/app/search/page.tsx"
|
|
309
|
+
export default function SearchPage() {
|
|
310
|
+
return (
|
|
311
|
+
<Suspense fallback={<p>Reading the query…</p>}>
|
|
312
|
+
<Filters />
|
|
313
|
+
</Suspense>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
React treats the throw as recoverable at the nearest boundary: the fallback is
|
|
319
|
+
what gets stored, and the browser renders the real value on hydration.
|
|
320
|
+
|
|
321
|
+
<Aside type="note" title="Without a boundary it still works, and that is the problem">
|
|
322
|
+
A root `loading.tsx` catches it, so the page is stored showing the fallback
|
|
323
|
+
the whole app shares — for a search page, the word "Loading" where the query
|
|
324
|
+
should be. The build says so rather than leaving you to find it:
|
|
325
|
+
|
|
326
|
+
```text
|
|
327
|
+
○ /search
|
|
328
|
+
⚠ nothing painted without the root loading.tsx — the fallback the whole
|
|
329
|
+
app shares is standing in for this page. Put a boundary where the
|
|
330
|
+
waiting is.
|
|
331
|
+
```
|
|
332
|
+
</Aside>
|
|
333
|
+
|
|
334
|
+
For a **server** component, the query arrives as a prop instead — `searchParams`,
|
|
335
|
+
awaited like `params`. Reach for the hook only when the component is
|
|
336
|
+
`"use client"` and has to follow navigations.
|
|
337
|
+
|
|
338
|
+
## Search params as state
|
|
339
|
+
|
|
340
|
+
[nuqs](https://nuqs.dev) is the usual answer for a filter or a tab that lives
|
|
341
|
+
in the url, and it works here with its own adapter:
|
|
342
|
+
|
|
343
|
+
```tsx title="src/app/layout.tsx"
|
|
344
|
+
import { NuqsAdapter } from '@rsc-kit/core/nuqs'
|
|
345
|
+
|
|
346
|
+
export default function RootLayout({ children }) {
|
|
347
|
+
return <NuqsAdapter>{children}</NuqsAdapter>
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
```tsx
|
|
352
|
+
const [kind, setKind] = useQueryState('kind')
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
The stock `nuqs/adapters/react` also works, for a **shallow** update — one
|
|
356
|
+
that changes the url and nothing else. Where it gives out is `shallow: false`,
|
|
357
|
+
the case where a server component reads the query and should render again with
|
|
358
|
+
the new one: with no router to call, it falls back to a full page load.
|
|
359
|
+
|
|
360
|
+
This adapter hands that case to the router, so the page's payload is refetched
|
|
361
|
+
in place and every bit of client state survives. It also tells our own
|
|
362
|
+
`useSearchParams` about a shallow change — that hook listens for a navigation
|
|
363
|
+
event rather than watching history, so an adapter that only writes the url
|
|
364
|
+
would leave the rest of the page reading the old query.
|
|
365
|
+
|
|
366
|
+
`nuqs` is an optional peer dependency. Nothing else imports it.
|
|
367
|
+
|
|
368
|
+
:::note[Where this should live]
|
|
369
|
+
In nuqs, alongside its adapters for Next, Remix and TanStack Router — that is
|
|
370
|
+
how every other framework's works, and the `custom` API this is built on is
|
|
371
|
+
their escape hatch for a framework that does not have one in-tree yet. It is
|
|
372
|
+
here until there are enough people using it to make that contribution a
|
|
373
|
+
reasonable ask. When `nuqs/adapters/rsc-kit` exists, this import will point at
|
|
374
|
+
it and then be removed.
|
|
375
|
+
:::
|
|
376
|
+
|
|
377
|
+
## Parallel routes
|
|
378
|
+
|
|
379
|
+
A directory prefixed with `@` is a slot. It renders alongside the page and
|
|
380
|
+
arrives as a named prop on the layout **in its own directory** — not on the
|
|
381
|
+
innermost one.
|
|
382
|
+
|
|
383
|
+
```text
|
|
384
|
+
src/app/
|
|
385
|
+
layout.tsx receives { children, sidebar, modal }
|
|
386
|
+
page.tsx arrives as "children"
|
|
387
|
+
@sidebar/
|
|
388
|
+
page.tsx arrives as "sidebar"
|
|
389
|
+
@modal/
|
|
390
|
+
default.tsx arrives as "modal" — empty until something fills it
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
```tsx title="src/app/layout.tsx"
|
|
394
|
+
export default function Layout({
|
|
395
|
+
children,
|
|
396
|
+
sidebar,
|
|
397
|
+
modal,
|
|
398
|
+
}: {
|
|
399
|
+
children: React.ReactNode;
|
|
400
|
+
sidebar: React.ReactNode;
|
|
401
|
+
modal: React.ReactNode;
|
|
402
|
+
}) {
|
|
403
|
+
return (
|
|
404
|
+
<div className="flex">
|
|
405
|
+
<aside className="w-[250px]">{sidebar}</aside>
|
|
406
|
+
<main className="flex-1">{children}</main>
|
|
407
|
+
{modal}
|
|
408
|
+
</div>
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
A layout that does not render a slot it was handed drops it silently — the page
|
|
414
|
+
comes out whole apart from the missing region, and nothing warns. Slots are
|
|
415
|
+
what [route interception](/guides/route-interception) fills.
|
|
416
|
+
|
|
417
|
+
## Streaming
|
|
418
|
+
|
|
419
|
+
On a full page load the HTML streams. React flushes the shell — layouts, static
|
|
420
|
+
markup, and every Suspense fallback in it — immediately, and injects the real
|
|
421
|
+
content as each boundary resolves.
|
|
422
|
+
|
|
423
|
+
SPA navigations stream too: the Flight payload is read with
|
|
424
|
+
`createFromReadableStream()` as it arrives, so React reconciles progressively
|
|
425
|
+
rather than waiting for the last byte.
|
|
426
|
+
|
|
427
|
+
## What the build stores
|
|
428
|
+
|
|
429
|
+
Nothing is declared. The build renders each route and stores what it can:
|
|
430
|
+
|
|
431
|
+
| Outcome | What happened |
|
|
432
|
+
| --- | --- |
|
|
433
|
+
| The whole page | It rendered to completion. Served from disk, no render per request. |
|
|
434
|
+
| A shell | Something was still waiting when the budget expired, but the static parts had already painted. The shell is stored; the rest is rendered per request and streamed in. |
|
|
435
|
+
| A redirect | The route only redirects. The redirect itself is stored — status and location — and served without rendering. |
|
|
436
|
+
|
|
437
|
+
There is no fourth outcome. A route that could not be stored, and did not say
|
|
438
|
+
it wanted to be rendered per request, **fails the build** and names the fix:
|
|
439
|
+
|
|
440
|
+
```text
|
|
441
|
+
Some routes could not be prerendered:
|
|
442
|
+
|
|
443
|
+
/photo/[id] — renders its params before it can paint, and lists no urls to build
|
|
444
|
+
|
|
445
|
+
Each one reads request data — params, headers, cookies, or the host —
|
|
446
|
+
above every Suspense boundary, so nothing can paint without it.
|
|
447
|
+
|
|
448
|
+
Put the part that waits inside <Suspense>, or add a loading.tsx beside
|
|
449
|
+
the page, so there is something to store while the rest arrives.
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
That is deliberate, and there is no flag to silence it. The alternative is a
|
|
453
|
+
category of routes that quietly render per request, which is where the slow
|
|
454
|
+
ones go to be forgotten.
|
|
455
|
+
|
|
456
|
+
See [Static generation](/guides/static-generation) for listing the urls of a
|
|
457
|
+
parameterised route, and [Partial prerendering](/guides/ppr) for where the
|
|
458
|
+
boundaries go.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Sections
|
|
2
|
+
|
|
3
|
+
> Refreshing one region of a page without re-rendering the rest.
|
|
4
|
+
|
|
5
|
+
A page often has one part that changes and a lot that does not. A section names
|
|
6
|
+
that part, so an action can refresh it on its own.
|
|
7
|
+
|
|
8
|
+
```tsx title="src/app/orders/orders.section.tsx"
|
|
9
|
+
import { section } from '@rsc-kit/core/section'
|
|
10
|
+
|
|
11
|
+
async function Orders() {
|
|
12
|
+
const orders = await db.orders()
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<ul>
|
|
16
|
+
{orders.map((o) => <li key={o.id}>{o.reference}</li>)}
|
|
17
|
+
</ul>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default section('orders', Orders)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Render it like any other component:
|
|
25
|
+
|
|
26
|
+
```tsx title="src/app/orders/page.tsx"
|
|
27
|
+
import Orders from './orders.section'
|
|
28
|
+
|
|
29
|
+
export default function OrdersPage() {
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<h1>Orders</h1>
|
|
33
|
+
<Orders />
|
|
34
|
+
</>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Refreshing it
|
|
40
|
+
|
|
41
|
+
An action names what it changed, and only that region is rendered again:
|
|
42
|
+
|
|
43
|
+
```tsx
|
|
44
|
+
'use server'
|
|
45
|
+
|
|
46
|
+
import { revalidate } from '@rsc-kit/core/revalidate'
|
|
47
|
+
|
|
48
|
+
export async function placeOrder(form: FormData) {
|
|
49
|
+
await db.orders.create({ reference: String(form.get('reference')) })
|
|
50
|
+
|
|
51
|
+
revalidate('orders')
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The rest of the page is untouched — not re-rendered and not re-fetched. Whatever
|
|
56
|
+
state lives outside the section, including a half-filled form beside it, stays
|
|
57
|
+
exactly as it was.
|
|
58
|
+
|
|
59
|
+
## The name is scoped to the module, not the app
|
|
60
|
+
|
|
61
|
+
Two pages may both call their section `orders`. The name is resolved through the
|
|
62
|
+
module the route declares, not through a table every section in the app writes
|
|
63
|
+
to.
|
|
64
|
+
|
|
65
|
+
That is a security property rather than a convenience. A name-keyed registry is
|
|
66
|
+
populated by every section at bundle load, so a lookup by name could reach any
|
|
67
|
+
page's region from any url — bounded only by whatever guard happened to sit on
|
|
68
|
+
the url that was asked for.
|
|
69
|
+
|
|
70
|
+
## What a section is not
|
|
71
|
+
|
|
72
|
+
It is not a cache boundary and not a client component. It renders on the server
|
|
73
|
+
like everything else; what it adds is a seam the server can render *into* on its
|
|
74
|
+
own, without producing the whole page around it.
|