Skip to content

Sections

Refreshing one region of a page without re-rendering the rest.

Updated View as Markdown

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.

src/app/orders/orders.section.tsxtsx
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

The 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:

src/app/orders/page.tsxtsx
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:

src/app/[team]/[app]/layout.tsxtsx
import { Suspense } from 'react'
import Header from './header.section'

export default function AppLayout({ params, children }) {
  return (
    <>
      <Suspense fallback={<HeaderSkeleton />}>
        <Header params={params} />
      </Suspense>
      {children}
    </>
  )
}
src/app/[team]/[app]/header.section.tsxtsx
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.
  • 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.tsx or <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.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close