In a React Server Components app, an interactive collapsible panel is a client component and needs to display a server component that queries the database. Why can't the panel import that server component, and what composition pattern lets the server-rendered content appear inside it?
answer
- import edge vs render edge
- the parent does the composing
- pass content, do not import it
- a hole the server fills
- client wrapper stays thin
basics
~20 sImporting it would pull the server component into the client bundle, so it could no longer run on the server. Instead a server parent renders it and passes it into the panel as children or a slot prop — already rendered, so it crosses as output, not as code.
solid answer
~50 sA client module's imports are client code. If the panel wrote `import { UserTable } from './UserTable'`, that module would be dragged across the boundary into the browser bundle, where it cannot `await` a database query and where its server-only imports do not belong — so the import is not allowed. The pattern that works is composition instead of import: a **server** parent renders both, and hands the server-rendered element to the client component as `children` (or any element-typed prop, which people call a slot): ``` <Panel><UserTable /></Panel> ``` `Panel` is a client component, but it never imports `UserTable`. It just places `{children}` in its output. React renders `UserTable` on the server, and only its *rendered result* crosses the boundary; the panel receives it as an opaque piece of already-rendered UI it can show, hide, or wrap. This is what "keep the boundary thin" means in practice.
code
javascript · 14 lines// Panel.jsx
'use client';
import { useState } from 'react';
export function Panel({ title, children }) {
const [open, setOpen] = useState(false);
return (
<section>
<button onClick={() => setOpen(!open)}>{title}</button>
{open && children}
</section>
);
}go deeper
Recall the shape of the fix: the server parent writes <Panel><UserTable /></Panel> and the client panel renders {children}. Say plainly that a client component cannot import a server component.
Explain the mechanism: importing would move the module into the client graph, while a child passed as a prop crosses as already-rendered output. Be ready to say that any element-typed prop works, not just children.
Frame it as bundle strategy — shrink the client component to an interactive shell with holes and keep the heavy, data-bound subtree above the boundary. Expect to justify the restructuring against a measured JavaScript budget.
Own the composition convention: interactive wrappers accept slots by default, and an import of substantial content from a client module is a reviewable design decision. Also be ready to say where the pattern stops and a request or navigation is the honest answer.
## The rule being enforced `'use client'` marks a module as an entry into the client graph, and everything that module imports goes with it. So the question "can a client component import a server component?" has a mechanical answer: no, because the act of importing would relocate that module to the client, and a module on the client cannot do the things that made it a server component — direct `await` of a data source, use of server-only credentials, shipping zero JavaScript. What is *not* forbidden is a server component appearing **inside** a client component in the rendered tree. Those are two different relationships, and confusing them is the whole point of this interview question: - **import** = a compile-time edge in the module graph. Blocked. - **render as a child** = a runtime edge in the element tree. Fine. ## The pattern: pass elements, not imports The composition happens one level up, in a server component that is allowed to import both sides. ```jsx // Page.jsx — a server component (no directive) import { Panel } from './Panel'; // client component import { UserTable } from './UserTable'; // server component export default function Page() { return ( <Panel title="Users"> <UserTable /> </Panel> ); } ``` ```jsx // Panel.jsx 'use client'; import { useState } from 'react'; export function Panel({ title, children }) { const [open, setOpen] = useState(false); return ( <section> <button onClick={() => setOpen(!open)}>{title}</button> {open && children} </section> ); } ``` `Panel` never names `UserTable`. It declares a hole. The server fills the hole. ## Why this is sound rather than a loophole When React renders `Page` on the server it evaluates `<UserTable />` there — the query runs on the server, the component's code stays on the server — and what it produces is a rendered result. That result is what gets handed to `Panel` as the `children` prop. From the panel's perspective `children` is just an opaque piece of UI: it can render it, hide it, or wrap it in a `<div>`, but it cannot inspect it, call into it, or re-render it. Nothing about `UserTable`'s implementation is needed in the browser, so nothing about it is shipped. A useful consequence: when the panel re-renders because its own state changed, `children` is the same value it was given. The server subtree does not re-run in the browser — there is no browser copy of it to run. ## Any element-typed prop works `children` is the ergonomic default, but there is nothing special about the name. A client component can accept several slots: ```jsx <Layout sidebar={<Nav />} main={<UserTable />} /> ``` As long as the value is an element the server produced, it crosses the boundary the same way. That is the difference candidates should be able to state: an *element* prop is rendered output, whereas naming the component type inside the client module would be an import. ## What this buys you: thin boundaries The reason interviewers like this question is that it is the practical technique for keeping client bundles small. The naive structure puts the interactive shell high in the tree and everything below it becomes client code by inheritance. The slot structure inverts it: the interactive shell is a small wrapper with holes, and the large, data-heavy, dependency-heavy content stays above the line on the server. Same rendered page; a fraction of the JavaScript. The habit to internalise is *pass content down, do not import it down*. When a client component starts wanting to import something substantial, that is the signal to turn the import into a prop. ## The limits worth naming The pattern moves rendered output, so it does not let a client component do anything dynamic with the server child. It cannot decide, at runtime in the browser, to render a *different* server component; the choices had to exist in what the server sent. If the interaction genuinely requires new server-rendered content on demand, that is a fetch or a navigation, not a slot — and the props you pass alongside the slot still have to be values that can cross the boundary at all, which is a separate constraint. ## The one-sentence answer A client component cannot import a server component, because importing it would make it client code — so a server parent renders it and passes it in as `children`, and only the rendered output crosses the boundary.
- When the client panel re-renders because its own state changed, does the server child re-render too?No. The panel received `children` as an already-rendered value; there is no copy of the server component's code in the browser to re-run. The panel's own output is recomputed and the same child value is placed back into it — which is exactly why this pattern also avoids re-render cost, not just bundle cost.
- Does the slot have to be called children, or can it be any prop?Any element-typed prop works — `sidebar={<Nav />}`, `header={<Title />}`. `children` is only the JSX-ergonomic version of the same thing. What matters is that the value is an element the server produced, rather than a component type the client module imported.
- Can the client component choose at runtime between two different server components to show?Only among the ones the server already sent. It can conditionally render slot props it was given, but it cannot ask for a server component that was not part of the payload — the code to render it does not exist in the browser. Genuinely new server-rendered content requires a navigation or a request.
- What is the smell that tells you a boundary has been drawn too high?A client module importing large, non-interactive, data-heavy modules. If the interactive part is a wrapper and everything meaty is below it in the same client graph, the wrapper should be shrunk to a shell with slots and the content lifted above the line into the server parent.
saying these in an interview costs you the question
- Says a client component can import a server component if it does not call hooks
- Thinks passing a component as children ships its code to the browser
- Confuses the import edge with the parent-child render edge
- Believes the server child re-runs when the client wrapper re-renders
- Solves it by making the whole subtree a client component