Course outline · 0% complete

0/29 lessons0%

Course overview →

Props across the boundary

lesson 3-4 · ~10 min · 10/29

Props that travel over a wire

There is one more rule of the server/client split, and it bites real teams weekly. When a server component renders a client component, the props it passes do not stay inside one program. The server serializes them, meaning it writes them down as data, sends them to the browser as part of the page payload, and React rebuilds them there.

So props crossing the server → client boundary must be serializable: strings, numbers, booleans, null, and plain arrays and objects made of those.

What cannot be written down and rebuilt: functions, class instances, database handles. Pass a function from a server component to a client component and Next.js stops you with the error every Next.js developer eventually meets:

Error: Functions cannot be passed directly to Client Components

Without this rule the framework would have to ship arbitrary server code to the browser, which is exactly what server components exist to prevent.

The broken delete button

A server component tries to hand its client child a callback:

// app/posts/page.js  (server)
import DeleteButton from "./delete-button"; // "use client"

export default async function Posts() {
  const posts = await getPosts();
  return posts.map((post) => (
    <DeleteButton
      key={post.id}
      onDelete={() => db.posts.remove(post.id)}  // ✗ function prop, build error
    />
  ));
}

The fix follows from the rule: send data across the boundary and keep behavior on the side where it runs.

// server: pass the id (a string, so it serializes cleanly)
<DeleteButton key={post.id} postId={post.id} />

Inside DeleteButton, the click handler is defined locally and calls the server through a proper channel, either a server action or an API route. Both are covered in Unit 6.

One deliberate exception exists. A server action may be passed as a prop, because Next.js sends a secure reference to it rather than the function's code.

Which props are legal across the boundary

A prop like post={{ title: "Hello", tags: ["react"] }} crosses without complaint. Props travel as serialized data, so plain objects of strings, numbers, booleans, arrays, and nested plain objects are all fine.

Functions, live database connections, and class instances are not. None of them can be written down as data and rebuilt in the browser, and shipping their code would leak server internals to every visitor. The single exception is a server action, which Next.js passes as a secure reference rather than as code.

Prop valueCrosses the boundary
"hello", 42, true, null
["react", "next"]
{ title: "Hi", tags: ["a"] }
() => save()
new Date() instance methods, class instances
an open database client
a server action✓ (as a reference)

canCrossBoundary: the serializability check as code

The check Next.js effectively runs on your props can be written out in plain JavaScript. canCrossBoundary(value) returns true when a value could be serialized and rebuilt in the browser: strings, numbers, booleans, null, and arrays or plain objects whose contents all pass the same test. It returns false for functions, and for anything that contains one at any depth.

function canCrossBoundary(value) {
  if (typeof value === "function") return false;
  if (Array.isArray(value)) return value.every(canCrossBoundary);
  if (value !== null && typeof value === "object") {
    return Object.values(value).every(canCrossBoundary);
  }
  return true;
}

console.log(canCrossBoundary({ title: "Hi", tags: ["react", "next"] }));
console.log(canCrossBoundary({ title: "Hi", onSave: () => {} }));
console.log(canCrossBoundary([1, "two", { ok: true }]));

Output

true
false
true

Reading the implementation

  • The function check comes first, because that is the hard no and no amount of recursion will rescue it.
  • The recursion is what makes the check deep: arrays are validated with value.every(canCrossBoundary), objects with Object.values(value).every(canCrossBoundary). A function buried three levels down still fails the whole prop.
  • typeof null === "object" in JavaScript, which is a long-standing quirk, so null has to be excluded before anything is treated as an object.

Fixing a formatDate prop the right way

A server component passes formatDate={(d) => d.toLocaleDateString()} to a "use client" component and the build fails. There are two tempting repairs, and only one of them is correct.

The right move is to define the formatting function inside the client component, or in a shared utility file that the client component imports, and pass only the date data across the boundary as a string or a timestamp.

The wrong move is to serialize the function's source code as a string and evaluate it in the browser. That is precisely what the framework forbids. It would ship server code to the client and break the security model that server components exist to provide.

Why it works out that way

  • The rule from this lesson: send data across the boundary, keep behavior on the side where it runs.
  • Behavior that the browser needs should originate in browser code, so it lands in the client bundle honestly instead of being smuggled in as data.