Passing Props Across the Boundary

Props Must Be Serializable

When a Server Component renders a Client Component, the props travel over the network inside the RSC payload. So they must be serializable: strings, numbers, booleans, null, plain objects and arrays, Date, Map, Set, Promises, JSX and Server Actions. Normal functions and class instances are not allowed.

// app/products/[id]/page.tsx (server)
import AddToCartButton from '@/components/AddToCartButton';

export default async function Page({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  return (
    <>
      <AddToCartButton productId={Number(id)} />            {/* OK: number */}
      {/* <AddToCartButton onAdd={() => console.log('x')} />  Error: functions cannot be passed */}
    </>
  );
}

What Survives the Trip

Example

React's format supports more types than JSON, but the idea is the same. Here JSON shows why functions and class methods disappear when data is sent from server to browser:

class Money {
  constructor(public paise: number) {}
  format(): string {
    return '₹' + (this.paise / 100).toFixed(2);
  }
}

const props = {
  id: 42,
  name: 'Redmi Note 14',
  tags: ['5G', 'AMOLED'],
  price: new Money(1799900),
  onAdd: () => 'added',
};

const received = JSON.parse(JSON.stringify(props));
console.log(Object.keys(props));
console.log(Object.keys(received));
console.log(typeof received.price.format);
console.log(received.price);

Output

[ 'id', 'name', 'tags', 'price', 'onAdd' ]
[ 'id', 'name', 'tags', 'price' ]
undefined
{ paise: 1799900 }

Tip: Pass plain data (ids, numbers, strings, plain objects). Convert class instances to plain objects in the Server Component first.

Server Components as Children

A Client Component cannot import a Server Component, but it can receive one as children. The server part is rendered on the server and slotted in.

// components/Tabs.tsx
'use client';
import { useState } from 'react';

export default function Tabs({ reviews, specs }: { reviews: React.ReactNode; specs: React.ReactNode }) {
  const [tab, setTab] = useState<'reviews' | 'specs'>('specs');
  return (
    <div>
      <button onClick={() => setTab('specs')}>Specifications</button>
      <button onClick={() => setTab('reviews')}>Reviews</button>
      {tab === 'specs' ? specs : reviews}
    </div>
  );
}

// app/products/[id]/page.tsx (server)
// <Tabs specs={<SpecsTable id={id} />} reviews={<ReviewList id={id} />} />