skip to content

In a Next.js App Router page, a Server Component loads a user row with 30 columns, renders the name as text, and passes only { id, name } into a Client Component. Which of that data actually reaches the browser, and how would you verify it?

level: middleimportance: must knowfreq 60%

answer

  1. only what crosses is copied
  2. rendered output versus the source row
  3. view-source, not the Elements panel
  4. the spread is what leaks the row
  5. props ship even when unused

basics

~20 s

Two things reach the browser: the markup the Server Component rendered, and a verbatim copy of the props handed to the Client Component. The other 28 columns stay on the server. Verify by searching the page source for a value you never passed.

solid answer

~50 s

A Server Component's own output is serialized as *rendered UI* — element types, host attributes and text — not as the data it fetched. So the name shows up because it was rendered, and the remaining columns never leave the server as long as nothing renders or passes them. Props handed to a Client Component are different: they are copied verbatim into the RSC payload so the browser can build that component during hydration, so `{ id, name }` travels as data the user can read. That payload is not hidden — Next inlines it into the HTML document in script tags that push onto `self.__next_f`, and serves the same payload for client-side navigations as a request with the `RSC` header and an `_rsc` query parameter. To verify, view the raw page source or open that response in the network panel and search for a column you believe you never exposed.

code

tsx · 13 lines
tsx
// app/users/[id]/page.tsx  (Server Component)
import { ProfileCard } from './profile-card' // 'use client'

export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
  const user = await getUser(id)

  // LEAKS: every column of `user` is encoded into the payload
  // return <ProfileCard {...user} />

  // SAFE: only these two values cross the boundary
  return <ProfileCard id={user.id} name={user.name} />
}

go deeper

for a junior

Know that anything handed to a client component as a prop is data the user can read in the page source, and that passing only the fields you need is the habit to build.

for a middle

Explain the two different outputs — rendered UI versus serialized props — and show that you can locate the payload in the raw document and in the navigation request rather than only describing it.

for a senior

Demonstrate the incident angle: name the spread as the realistic cause, describe how you would grep a deployed response for a field, and say why an unused or conditionally rendered prop is already exposed.

for a principal

Own the convention that a boundary carries decisions and projections, not whole records, and put a mechanical check for it into review or CI so a single careless spread cannot leak a table's worth of columns.

## Two different things travel It helps to stop thinking of "the data" as one blob. A Server Component render produces two kinds of output, and only one of them is your source data. **Rendered UI.** When a Server Component returns `<p>{user.name}</p>`, what is emitted is a host element with a text child. The 30-column row was an intermediate value in a function that already finished. Nothing in the output references it. This is the property that makes Server Components attractive in the first place: you can query wide rows, join tables, call an internal service, and only the shape you rendered leaves the machine. **Client Component props.** When the same render returns `<Profile user={{ id, name }} />` and `Profile` is a Client Component, that element cannot be executed on the server. It is written into the payload as a module reference plus an encoded copy of its props, because the browser has to construct the component itself. Those props are user-visible data, not an internal detail. The rule of thumb: **rendered means the user sees it; passed means the user can read it.** Both reach the browser; they differ in shape, not in exposure. ## Where the payload physically is On an initial document request, Next streams HTML and the RSC payload in the same response. The payload arrives as a series of inline scripts that push chunks onto a global array (`self.__next_f`); the exact global name is an implementation detail, but its presence in view-source is not. On a client-side navigation there is no new HTML document at all — the router fetches only the payload for the new segment, a request Next marks with the `RSC` header and an `_rsc` query parameter, and that response is held in the client router cache. That gives you three concrete inspection points: 1. `view-source:` on the page and search for a value. Not devtools' Elements panel — that shows the hydrated DOM, which hides the script payload. 2. `curl` the route and grep the body. 3. Devtools network panel, filter for the request with `_rsc` in the query string, and read the response. A one-line check that catches most incidents: grep the raw response for a value that should never be public — an email domain, a hash prefix, an internal flag name. ## The failure this question is really about The realistic version is not deliberate; it is a spread: ```tsx const user = await db.user.findUnique({ where: { id } }) // only the name is rendered, but everything is handed over return <ProfileCard {...user} /> ``` or its cousin, `<ProfileCard user={user} />`, where `ProfileCard` reads two fields. The component uses two fields; the payload carries thirty, including whatever the table happens to hold — a password hash, an internal risk score, a partner's identifier, a soft-deleted address. Nothing throws, nothing looks wrong on screen, and the data sits in the HTML of every rendered page. Two further sharp edges: - **Serialization does not care whether the prop is used.** A prop the client component ignores is still encoded and still shipped. - **A conditional in the client component does not protect anything.** `{isAdmin && <Secret value={user.ssn} />}` still transmitted `user.ssn`; the browser decided not to render it. ## What to do instead Project at the boundary, deliberately and close to the crossing: ```tsx const user = await db.user.findUnique({ where: { id } }) return <ProfileCard id={user.id} name={user.name} /> ``` Narrowing the query itself (selecting only the columns you need) is better still, because then the wide row never exists in the render at all and no future edit can accidentally spread it. When a client component genuinely needs a derived value that depends on private data, compute the derived value on the server and pass *that* — pass `canEdit: true`, not the role list and the ownership record it was derived from. ## The mental model to state in an interview Say it as an asymmetry: a Server Component's data is private by default and becomes public the moment it is rendered or passed; a Client Component's props are public by construction, because they are transport, not memory. Then say how you check — because the checkable part is what separates someone who has debugged this from someone who has read about it.

  • Does a prop that the client component never renders still reach the browser?
    Yes. Encoding happens when the element is written into the payload, before the component runs and regardless of what it does with the value. A prop that is ignored, or rendered only behind a false condition, has already been transmitted. The only way to keep a value out is not to pass it.
  • Someone argues the data is safe because the component renders on the server. What do you say?
    Where the component *renders* is unrelated to what it *transmits*. A Client Component is server-rendered to HTML on the first request, but its props are still serialized into the payload so hydration can rebuild it in the browser. Server rendering is a performance mechanism, not a confidentiality one.
  • How would you check this on a deployed route rather than locally?
    Request the route and read the raw body — `curl` the URL, or use view-source in the browser, then search for the field. In devtools, filter the network panel for the navigation request carrying the `_rsc` query parameter and read that response. The Elements panel is not a valid check: it shows the hydrated DOM, not the inlined payload.
  • If a Server Component computes a permission from a role list, what should cross the boundary?
    The decision, not the inputs. Pass `canEdit: true` and keep the role list, ownership record and policy rules on the server. That reduces payload size, avoids exposing internal authorization structure, and prevents the client from re-deriving the rule — the client should be told what it may show, not given the material to decide.

saying these in an interview costs you the question

  • Thinks server-fetched data is invisible unless explicitly rendered
  • Assumes the payload is internal and not readable by users
  • Spreads the whole database row into a client component
  • Checks the Elements panel instead of the raw response
  • Believes an unrendered prop is never transmitted

context