Skip to main content
ASoc
Tutorial

Next.js: Pass Props to a Component Across the Client Boundary

Props are React; the serialization boundary is Next.js. The four shapes a 92-component codebase uses, including a Server Action and three server-rendered slots.

The ASoc Team8 min read

In Next.js you pass props exactly as in React — <Child label="Buy" />, destructured in the child's parameter list. What Next.js adds is a boundary: props crossing from a Server Component into a Client Component must be serializable, so a plain function fails, a Server Action is allowed, and already-rendered JSX passes through as a slot.

Every tutorial for this keyword stops at the first sentence. The second is the one that produces real errors, and it is not a React rule — it exists because a Server Component's output is serialized and sent to the browser, where the client component is hydrated. This post walks the four prop shapes a production codebase actually uses, from a 92-component Next.js storefront where 24 components are client components and 68 are not.

The client boundary, measured

"use client" is a prop rule in disguise: it decides which props are legal on a component. Here is where the boundary actually sits in this repository:

LayerClient componentsTotal
src/components/atoms07
src/components/molecules1941
src/components/organisms533
src/components/templates011

Nearly 80% of the client components live in one layer, and it is the small-unit layer: the accordion, the gallery, the modal, the forms. Atoms are pure markup and templates only order sections, so neither needs state. That shape is not an accident — it is what keeps the serialization question contained to one place, because the only props that have to be serializable are the ones entering those 24 files.

Shape 1 — plain data, the 90% case

// src/components/molecules/PurchaseRow.tsx — a server component, prop-driven
export default function PurchaseRow({
  tierName,
  purchasedOn,
  amountLabel,
  status,
  receiptUrl,
  action,
}: {
  tierName: string;
  purchasedOn: string;
  amountLabel: string | null;
  status: "paid" | "refunded";
  receiptUrl: string | null;
  action?: ReactNode;
}) {

Strings, numbers, booleans, null, arrays and plain objects cross the boundary without complaint. Note amountLabel: string | null rather than a number plus a formatter prop: formatting happens on the server, and what crosses is the finished string. Pushing the computation to the server side of the boundary is the cheapest way to keep a prop serializable.

There is a house rule attached to this shape: molecules never import runtime values from data files. Type-only imports are fine; the organism reads the data and passes the result down. A component that imports its own content cannot be reused, and worse, a client component that imports a data module drags that module into the browser bundle.

Shape 2 — a Server Action as a prop

This is the exception people meet as an error message. You cannot pass an arbitrary function from a server parent to a client child, but you can pass a Server Action, because it is serialized as a reference the client calls back to:

// src/components/molecules/AuthCard.tsx — "use client"
export default function AuthCard({
  action,
  redirectTo,
  children,
}: {
  action: (prev: AuthState, formData: FormData) => Promise<AuthState>;
  redirectTo?: string;
  children: ReactNode;
}) {
  const [state, formAction, pending] = useActionState<AuthState, FormData>(
    action,
    null,
  );

AuthCard is shared chrome for four routes — login, signup, forgot-password, reset-password. Each page passes its own action (signInWithPassword, signUpWithPassword, requestPasswordReset, updatePassword) from src/lib/actions/auth.ts and its own field markup as children. The client component owns useActionState and the pending state; it knows nothing about which action it is driving.

The rule in one line: a function prop is legal if it is a Server Action, or if the parent is itself a client component. The second half is why PreviewModal can take onClose: () => void — its caller, TemplateCard, starts with "use client", so no boundary is crossed and the function is just a closure.

Shape 3 — rendered JSX as a prop (the slot)

The most useful Next.js-specific pattern, and the one that is hardest to find written down: you can render a tree on the server and pass it as a prop into a client component. What crosses the boundary is the serialized output, not the components.

// src/app/dashboard/page.tsx — three server-rendered trees as props
<DashboardTabs
  initialTab={isValidTab(tab) ? tab : "overview"}
  overview={overviewContent}
  purchases={purchasesContent}
  settings={settingsContent}
/>
// src/components/organisms/DashboardTabs.tsx — "use client"
export default function DashboardTabs({
  initialTab,
  overview,
  purchases,
  settings,
}: {
  initialTab: DashboardTab;
  overview: ReactNode;
  purchases: ReactNode;
  settings: ReactNode;
}) {
  const [activeTab, setActiveTab] = useState<DashboardTab>(initialTab);
  const content: Record<DashboardTab, ReactNode> = {
    overview,
    purchases,
    settings,
  };

All three panels are fetched and rendered server-side up front. The client component's entire job is deciding which one is visible — so switching tabs never re-fetches and never shows a loading state, and the data-access code (Supabase queries, the entitlement engine) stays on the server where it belongs.

The same pattern appears twice more for a different reason — keeping a component presentational:

  • PreviewModal takes actions?: ReactNode. Callers drop a purchase CTA and a save heart into it on marketing pages and nothing in the dashboard. The comment in the file puts it plainly: keeping it a ReactNode "is what stops commerce state leaking into a component whose whole job is to frame an iframe."
  • PurchaseRow takes action?: ReactNode so a server component can host a client island (the refund button) in its footer without becoming a client component itself.

A slot prop is how you keep the boundary where you drew it instead of letting "use client" spread upward through the tree.

Shape 4 — children, which is just a prop

children is the slot with the nicest syntax, and the blog you are reading uses it at the page level: ArticleBody takes children: ReactNode and receives the compiled MDX. One prose column, 373 posts, zero per-post layout code.

Troubleshooting

Error or symptomCauseFix
"Event handlers cannot be passed to Client Component props"A function defined in a server component passed to a client childMove the handler into the client component, or make the parent "use client"
"Functions cannot be passed directly to Client Components"Same boundary, non-handler function (a formatter, a comparator)Call it on the server and pass the result, or make it a Server Action
"Only plain objects can be passed to Client Components"A class instance, Map, Set, Date in a nested field, or a Supabase clientMap to a plain object on the server; send ISO strings, not Date objects
Props are undefined in a page componentparams/searchParams are promises in current Next.jsconst { slug } = await params
Child re-renders on every parent renderA fresh object/array literal in the propHoist the literal, or memoize if it is genuinely expensive
A client component's bundle is far larger than its codeIt imports a data module or a heavy library for one valueHave the parent compute the value and pass it as a prop
TypeScript rejects a valid ReactNode slotProp typed JSX.ElementWiden it to ReactNode — strings, arrays and null are all valid children

FAQ

How do I pass props to a component in Next.js? The same way as in React: attributes on the JSX tag, destructured in the child's parameters, typed with an inline object type or an interface. The Next.js-specific part is which values are allowed when the parent is a Server Component and the child is a Client Component — serializable data yes, Server Actions yes, rendered JSX yes, arbitrary functions and class instances no.

Can I pass a function as a prop in Next.js? Yes, in two cases. If both components are client components, a function prop is an ordinary closure — onClose: () => void is used that way here. If the parent is a Server Component, the function must be a Server Action ("use server"), which is passed as a reference the client invokes over the network. Anything else throws at render time.

How do I pass data from a Server Component to a Client Component? Fetch on the server, pass plain serializable values down. If the payload is big or the shape is awkward, render it on the server and pass the rendered tree as a ReactNode prop instead — that is what the dashboard does with all three of its tab panels. Both beat re-fetching the same data from the client.

Should I use props or Context for deep trees? Props, until the threading genuinely hurts. Context requires a client component at the provider, which pulls the boundary up the tree and takes everything below it into the browser bundle. In 92 components here the answer has been props plus slots every time; the thing that made it bearable is that organisms compute and molecules only receive.

Templates in this post

ASoc Ledger (a finance-app site with live dashboards), ASoc Lens (a product-analytics SaaS site) and ASoc Magnet (a lead-capture SaaS site) are built on the same five-layer, prop-driven component split described above — atoms and templates with no client code, state confined to the small-unit layer.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates. For the React-level mechanics underneath this, see React passing props and pass a component as a prop; for where the boundary itself is drawn, server components vs client components.

Keep reading

Tutorial8 min read

Next.js Prefetch: What Fires on Viewport Entry, Not Just on Click

Link prefetches on scroll into view, before anyone clicks. This storefront keeps one href out of Link entirely because a prefetch there would burn a buyer's download quota.

Read more
Tutorial10 min read

Product Filtering in Next.js: Why Filters Belong in the URL

Filters in useState cannot be shared, bookmarked, or server-rendered. How to read them from searchParams — and which filtered URLs to let Google crawl.

Read more
Tutorial11 min read

A Product Image Gallery in Next.js That Google Can Actually See

Virtualizing a four-slide carousel removes three product images from the HTML. Mount every slide, starve the off-screen ones, and the ARIA that a carousel actually needs.

Read more