skip to content

In a Next.js App Router app, is `children` the only prop through which server-rendered content can reach a Client Component, and what is the rule that makes it work?

level: middleimportance: should knowfreq 42%

answer

  1. children is only a prop name
  2. named slots: left, right, header
  3. who creates the element decides
  4. element, not a component reference
  5. slot cannot be re-run in the browser

basics

~20 s

Any prop can carry it. children is just the conventional name — a Server Component can pass JSX through named props like header or sidebar too. The rule is that the element must be created by the server parent, not imported by the client component.

solid answer

~50 s

`children` is only a convention. Any prop can carry a React element, so a Server Component can write `<SplitPane left={<Chart />} right={<Table />} />` where `SplitPane` is a client component and both `Chart` and `Table` are server components. The rule that matters is *who creates the element*: as long as the server parent creates it and passes it down, the client module never imports the server module, so nothing about that subtree crosses the boundary as code — it crosses as already-rendered output. What the client component may do with it is limited: render it, wrap it, show or hide it. It cannot call it with different props or re-run it in the browser, because server components do not execute there. New server output requires a fresh server render — a navigation, or `router.refresh()` from `next/navigation`.

code

tsx · 16 lines
tsx
import type { ReactNode } from 'react'
import { SplitPane } from './split-pane'
import { RevenueChart } from './revenue-chart'
import { LedgerTable } from './ledger-table'

// Server Component: it creates both elements, so neither module
// is ever imported by the client SplitPane.
export default function ReportsPage() {
  return (
    <SplitPane
      left={<RevenueChart />}
      right={<LedgerTable />}
      footer={<p>Figures refresh on navigation.</p> as ReactNode}
    />
  )
}

go deeper

for a junior

Know that a Server Component can hand rendered UI to a Client Component through any prop, and that you pass JSX — <Panel /> — not the component function itself.

for a middle

State the rule precisely: the client module must not import the server component; the server parent creates the element and the client side receives finished output through a prop of any name.

for a senior

Show the design consequence — interactive components take content in through slots rather than importing or fetching it, and any need for different server content is answered by a navigation or router.refresh(), not by the client re-rendering the slot.

for a principal

Own it as a component-API standard: shared interactive primitives are defined by their slots, so they stay usable from any route without dragging feature code across the boundary.

## Named slots, not just children Many people learn the composition pattern as "pass server components as `children`" and stop there. That undersells it. `children` is nothing more than the prop React fills in from the JSX between a component's tags; it has no special powers. Any prop can hold an element: ```tsx // app/reports/page.tsx — a Server Component import { SplitPane } from './split-pane' // 'use client' import { RevenueChart } from './revenue-chart' // server, hits the DB import { LedgerTable } from './ledger-table' // server, hits the DB export default function ReportsPage() { return ( <SplitPane left={<RevenueChart />} right={<LedgerTable />} /> ) } ``` `SplitPane` is a client component with drag state. `RevenueChart` and `LedgerTable` render on the server, query the database, and never enter the browser bundle. This is what lets a real design system expose interactive containers — tabs, resizable panes, accordions, modals — that hold server-rendered content in several distinct regions rather than one. ## The rule underneath The boundary is a module-graph rule. A module marked `'use client'` is an entry point, and everything it *imports* becomes client code. So the question for any composition is simply: **does the client module import the server component?** - If `split-pane.tsx` imported `RevenueChart` directly, `RevenueChart` would be pulled into the client graph — and a server component cannot function there. - Because the server *page* imports both and creates the elements, the client module only ever sees a prop. It has no idea what is inside. During the server render, that subtree is rendered and serialized into the RSC payload. The client component receives a reference to that finished output and drops it into its own tree wherever it renders that prop. ## What the client component can and cannot do with it Can: - render it once, or in a conditional branch (`{open && header}`), or inside its own wrapper markup; - decide *where* in its layout the slot goes; - render it more than once — it is just an element. Cannot: - re-invoke the server component with different props, since the function never reaches the browser; - get updated server data by re-rendering itself — the same output stays there until a new server render happens; - inspect it usefully to make decisions about its contents. When the client component genuinely needs *different* server-rendered content in response to interaction, the answer is not to reach into the slot. It is either to have the server parent pass several slots and let the client component switch between them, or to trigger a real server render — navigate to a URL that carries the new state, or call `router.refresh()` after a mutation. ## Why this shape matters for design Once you see element props as slots, a useful discipline follows: an interactive component should be written to take content in, not to fetch or import it. That keeps it small, keeps it reusable across routes with completely different content, and keeps the data access in the server components that own it. The alternative — the client component importing what it needs — is what drags whole feature subtrees into the browser one import at a time. The same reasoning applies to props that are not elements but *hold* elements, such as an array of `{ id, label, content }` where `content` is JSX. That works for the same reason, and it is how a tab list with server-rendered panels is usually typed. ## Common mistakes - Believing the pattern is a special case of `children` rather than a general property of props. - Passing a *component reference* instead of an element (`content={ServerPanel}` rather than `content={<ServerPanel />}`): now the client module holds a function it would have to call itself, which is exactly what cannot happen across the boundary. - Expecting the client component to be able to refresh a slot on its own.

  • What breaks if you pass the server component itself, as in content={ServerPanel} instead of content={<ServerPanel />}?
    You have handed the client module a function it would have to call during its own render — in the browser — which server components cannot do. Passing `<ServerPanel />` instead means the server parent invokes it during the server render and only the finished output crosses. Pass elements, never component references.
  • A client tab bar needs each tab's server-rendered panel. How do you shape that?
    Have the server parent render all the panels and pass them in as slots — an array of `{ id, label, panel }` where `panel` is JSX — and let the client component switch which one it displays. If rendering every panel up front is too expensive, make the tabs navigate to real URLs so each panel is a separate server render.

saying these in an interview costs you the question

  • Thinks only the children prop can carry server-rendered content
  • Passes a component reference instead of an element across the boundary
  • Says the client component can re-render the slot with new props
  • Believes the client component needs to import the server component to render it
  • Expects a slot to refresh itself without a new server render

context