@rsc-kit/mcp
Version:
An MCP server over what an rsc-kit build decided: the routes, why each one is static or not, and what it costs the browser.
159 lines (118 loc) • 5.44 kB
Markdown
# Sections
> Refreshing one region of a page without re-rendering the rest.
A page often has one part that changes and a lot that does not. A section names
that part, so an action can refresh it on its own.
```tsx title="src/app/orders/orders.section.tsx"
import { section } from '@rsc-kit/core/section'
async function OrdersList() {
const orders = await db.orders()
return (
<ul>
{orders.map((o) => <li key={o.id}>{o.reference}</li>)}
</ul>
)
}
const Orders = section('orders', OrdersList)
export default Orders
```
Give the section a name before exporting it, as above, rather than writing
`export default section(...)`. An editor offers an import by the name a default
export has, and a call has none: it falls back to one made up from the file name,
`OrdersSection`, so typing `<Orders` finds nothing to import.
Render it like any other component:
```tsx title="src/app/orders/page.tsx"
import Orders from './orders.section'
export default function OrdersPage() {
return (
<>
<h1>Orders</h1>
<Orders />
</>
)
}
```
## Refreshing it
An action names what it changed, and only that region is rendered again:
```tsx
'use server'
import { revalidate } from '@rsc-kit/core/revalidate'
export async function placeOrder(form: FormData) {
await db.orders.create({ reference: String(form.get('reference')) })
revalidate('orders')
}
```
The rest of the page is untouched — not re-rendered and not re-fetched. Whatever
state lives outside the section, including a half-filled form beside it, stays
exactly as it was.
### From an action called on another page
An action says what it changed, not which page called it. A backend's
action cannot know at all: Go's `rsckit.Revalidate(ctx, "orders")` and
Laravel's `Rsc::revalidate('orders')` name the data. When the page that
called the action has no `orders` section - its own or one beside its layouts -
there is nothing on screen to refresh, and the name is skipped; the action still answers. A name no page
in the app has is a mistake, and the action fails, naming the sections and
slots the calling page does have.
## In a layout
A layout stays mounted while you move between the pages under it. So the part of
a screen that is the same on every tab - a title, a status, the tab bar - belongs
there, where it does not go away and come back when a tab is clicked. When that
part shows data that changes, make it a section, and any page under the layout
can refresh it by name:
```tsx title="src/app/[team]/[app]/layout.tsx"
import { Suspense } from 'react'
import Header from './header.section'
export default function AppLayout({ params, children }) {
return (
<>
<Suspense fallback={<HeaderSkeleton />}>
<Header params={params} />
</Suspense>
{children}
</>
)
}
```
```tsx title="src/app/[team]/[app]/header.section.tsx"
import { section } from '@rsc-kit/core/section'
async function AppHeader({ params }) {
const { team, app } = await params
const status = await db.status(team, app)
return <header>{status}</header>
}
const Header = section('header', AppHeader, {
refreshOn: ({ params }) => [`app:${params.app}`],
})
export default Header
```
The header reads `params`, so it has its own `<Suspense>`: the layout paints with
the skeleton at once and the header streams in. A layout that awaits `params`
above every boundary is refused at build, on a route that lists no urls.
`revalidate('header')` from an action called on any page under the layout
refreshes it, and so does `refreshOn`. A page's sections are the ones beside it
and then the ones beside each of its layouts, nearest first, so:
- **The name is unique along the chain.** A page's `header` and its layout's are
one name on one screen; the nearer one is the one a refresh renders.
- **A refresh renders the section with the page's props**, `params` and
`searchParams`, awaitable as the page's own are, not the ones the layout
passed. Pass `params` through, as above, and read what the section needs from
it: a page's `params` hold every segment the layout's do.
- **The active tab is not a refresh.** Mark it from `usePathname()` in a client
component inside the section; it follows the url on every navigation.
A tab switch then replaces only the page. The header is already there.
## Refreshing on a change from outside
An action refreshes the sections it changed. For a change made anywhere else -
a webhook, a job, another visitor - a section can name what it depends on and
refresh itself in every open tab: `section('repos', Repos, { refreshOn })`.
That has its own guide: [Live data](/guides/live-data).
## The name is scoped to the module, not the app
Two pages may both call their section `orders`. The name is resolved through the
modules the route declares - the sections beside the page and beside its
layouts - not through a table every section in the app writes to.
That is a security property rather than a convenience. A name-keyed registry is
populated by every section at bundle load, so a lookup by name could reach any
page's region from any url — bounded only by whatever guard happened to sit on
the url that was asked for.
## What a section is not
It is not a cache boundary and not a client component. It renders on the server
like everything else; what it adds is a seam the server can render *into* on its
own, without producing the whole page around it.