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.
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 OrdersThe name passed to section() is the file’s: orders.section.tsx is the
section orders, and revalidate('orders') is how an action refreshes it. The
renderer finds a section by its file and the browser by the name it was given,
so a section whose two names differ would refresh nothing; the build refuses
one, and says which file to rename.
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:
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:
'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:
import { Suspense } from 'react'
import Header from './header.section'
export default function AppLayout({ params, children }) {
return (
<>
<Suspense fallback={<HeaderSkeleton />}>
<Header params={params} />
</Suspense>
{children}
</>
)
}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 HeaderThe 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
headerand 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,
paramsandsearchParams, awaitable as the page’s own are, not the ones the layout passed. Passparamsthrough, as above, and read what the section needs from it: a page’sparamshold every segment the layout’s do. - A refresh keeps the section on screen until it is ready. The new
content is swapped in as a transition, so a section whose refresh is still
arriving keeps showing what it showed - it does not fall back to the nearest
loading.tsxor<Suspense>skeleton on every save. One that is ready shows at once. - A refresh lasts as long as its layout. Moving to another page under the layout keeps what the refresh rendered; a navigation that renders the layout again - to a page under a different one - renders the section with it.
- 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.
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.