UNPKG

@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
# 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.