The Wire & The Wall: Serialization & The Flight Protocol

Why you cannot pass functions to Client Components. A deep dive into the React Flight Protocol, the limits of JSON, and the security implications of the RSC payload.

By Satish Kumar December 11, 2025

In Part 1, we separated React into two distinct phases: The Architect (Render Phase) and The Builder (Commit Phase).

In the new world of React Server Components (RSC), the Architect lives on the Server, and the Builder lives in the Browser.

This creates a massive physical problem. The Architect cannot just "hand" the blueprints to the Builder. They are separated by miles of fiber optic cable, routers, and latency. They need a communication protocol.

Most developers assume this protocol is just HTML or JSON. They are wrong. It is something much more powerful, and much more restrictive.

We need to talk about The Wire.

The Constraints of Douglas Crockford

To understand RSC, we must look back to 2001, when Douglas Crockford popularized JSON.

JSON is the lingua franca of the web. It is perfect for data. But React components aren't just data; they are complex trees containing Date objects, Maps, Sets, and most importantly, Promises.

If you try to JSON.stringify a standard React component tree, it falls apart:

  1. Dates turn into strings ("2025-12-10").
  2. Maps/Sets turn into {}.
  3. Functions vanish entirely.
  4. Promises cannot be serialized.

The React team, led by engineers like Dan Abramov and Sebastian Markbåge, realized that standard JSON wasn't enough to describe a UI tree. They needed a protocol that could stream React Elements, resolve references, and handle lazy-loaded code chunks.

They created The Flight Protocol.

Reading the Matrix: The Flight Payload

You have likely seen this payload before, perhaps while debugging a network request in Chrome DevTools. It looks like broken JSON.

1:I["./app/components/Counter.js",["client","app/components/Counter","default"]]
2:"$Sreact.suspense"
0:["$","div",null,{"children":[["$","h1",null,{"children":"Hello"}],["$","L1",null,{"count":10}]]}]

This is not random noise. It is a highly optimized, line-delimited instruction set for the Browser.

  • 1:I (Import): "Hey Browser, download the code bundle for Counter.js. I'm going to refer to it as ID #1."
  • 0: (The Root): "Here is the main tree. It's a div. Inside, put an h1."
  • $L1 (Lazy Reference): "Right here, render the component from ID #1. And pass it these props: { count: 10 }."

This is the crucial realization: The Server is not sending HTML. It is sending a description of the UI, mixing static data (Strings) with dynamic references (Client Component filenames).

Because it is line-delimited, React can parse this as it arrives. It doesn't need to wait for the entire response. This is what enables Streaming—the browser can start building the UI while the backend is still fetching data for later lines in the payload.

The Wall: Why Functions Die

This brings us to the most common error in Next.js/RSC:

Why?

When you define a function on the Server, it exists in the Server's Memory. It might depend on variables in that scope (Closures).

// Server Component
export default function Page() {
  const secretKey = process.env.SECRET_KEY; // Exists on Server

  // This function is a "Closure". It captures 'secretKey'.
  const handleClick = () => {
    console.log(secretKey);
  };

  // ❌ CRASH: You are trying to teleport a function
  return <ClientButton onClick={handleClick} />;
}

To send handleClick to the browser, React would have to serialize the function's code AND the entire server environment (including secretKey). This is a massive security risk and technically impossible.

The Rule: You can pass Data across the wall. You cannot pass Behavior.

The "Loophole": JSX is Data

If we can't pass standard functions, how do we compose apps?

We learned in Part 1 that the Render Phase produces a JSON-like tree (React Elements). Since <div /> is just an object ({ type: 'div', ... }), it is serializable.

This allows us to pass Server Components as props to Client Components. This is widely known as the "Slot Pattern".

// Server Component
const content = <ServerProfile />; // This is just an Object!

// Client Component
return <Modal>{content}</Modal>; // ✅ Allowed

The Flight Protocol treats this nested component as just another piece of data ($L2) in the stream. The Client Component receives the "Hole" where the content should go, but it never sees the server-side code that generated it.

The Security Audit

Because the Flight Payload is just text, it is Public Information.

Senior Engineers treat the Network tab like a public billboard. If you pass a prop to a Client Component, you are publishing it to the world.

The Leaky Prop

// Server Component
export default function UserPage({ user }) {
  // User object: { id: 1, name: "Satish", passwordHash: "xyz123" }

  // ❌ DANGER: Even if <Profile> doesn't render the hash,
  // it is now in the browser's network traffic.
  return <Profile user={user} />;
}

In the old days (Server-side rendering with EJS/PHP), variables stayed on the server unless you explicitly printed them. In RSC, props are the transport layer. If you prop it, you ship it.

Summary

  1. The Protocol: RSC uses "Flight," a text format that can describe UI trees and resolve references to Client Bundles.
  2. The Wall: You can only pass things that can be turned into text (JSON + specialized React types).
  3. No Functions: Closures cannot cross the network boundary. Server Actions work via references, not serialization.
  4. Leakage: Anything passed to a Client Component is visible to the user, even if not rendered visually.

Now that we know how the Architect talks to the Builder, we need to talk about the cost of waking the Builder up. In the next article, we dive into Hydration.


Challenge: The Security Audit

You are reviewing a Pull Request. The developer has written the following code to toggle a generic "Admin View" for a user.

Task: Explain why this code is a security vulnerability, specifically referencing the Flight Payload.

// app/user/[id]/page.tsx (Server Component)
import db from '@/lib/db';
import AdminPanel from './AdminPanel'; // Client Component

export default async function Page({ params }: { params: { id: string } }) {
  const user = await db.user.findUnique({
    where: { id: params.id },
  });

  // user object includes:
  // { id: "1", name: "Alice", isAdmin: false, twoFactorSecret: "H829..." }

  if (!user) return 404;

  return (
    <main>
      <h1>Welcome {user.name}</h1>
      {/* The developer says: "It's safe, the AdminPanel checks isAdmin prop!" */}
      <AdminPanel userData={user} isAdmin={user.isAdmin} />
    </main>
  );
}
Click to Reveal Solution

The Vulnerability: The developer passed the entire user object (aliased as userData) to the Client Component.

The Exploit: An attacker (or even the user Alice) can open the Network tab, find the RSC payload, and search for the userData object. They will see twoFactorSecret: "H829..." in plain text.

The Fix: Only pass the specific fields needed. userData={{ name: user.name }}.


Subscribe to my free Newsletter

Join my weekly newsletter to get the latest updates on design engineering, new projects, and articles I've written. No spam, unsubscribe at any time.