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.
Part of the series: The Senior Engineer's Guide to React Server Components
- Part 1The Architect & The Builder: React’s Internal Split
- Part 2The Wire & The Wall: Serialization & The Flight Protocol
- Part 3The Cost of Waking Up: Hydration & The Uncanny Valley
- Part 4The Great Split: Zero-Bundle-Size Architecture
- Part 5The Hole in the Donut: RSC Composition Patterns
- Part 6Breaking the Waterfall: Streaming & Suspense
- Part 7The Loop: Server Actions & The RPC Revival
- Part 8The Senior Playbook: Architecture, Patterns & Caching
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:
- Dates turn into strings (
"2025-12-10"). - Maps/Sets turn into
{}. - Functions vanish entirely.
- 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 forCounter.js. I'm going to refer to it as ID#1."0:(The Root): "Here is the main tree. It's adiv. Inside, put anh1."$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:
Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with 'use server'.
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.
But wait, what about Server Actions?
You might be thinking: "I pass Server Actions to forms all the time. Those are functions!"
Server Actions are a special exception. When you pass a Server Action prop, React doesn't serialize the function code. Instead, it serializes a Reference ID (a URL endpoint). When the client calls that function, it's actually making a POST request to the server to execute the logic there.
You aren't passing the function; you are passing a remote control to it.
We will learn more about server actions in later part of the series.
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>; // ✅ AllowedYou cannot import a Server Component into a Client Component file. Instead, you pass the Server Component as a prop (usually children) from a parent Server Component (like a Page or Layout).
1. When Imported: You transfer the Source Code. The bundler packages the Server Component into the client's JavaScript bundle. If that component contains server-only logic (like database connections or API keys), the browser tries to run it and crashes.
2. When Passed as a Prop: You transfer the Rendered Result. The Server Component executes entirely on the server first. It converts into a static format (serialized JSON/HTML), and only that visual result is handed to the Client Component. The server code stays safe on the server.
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
- The Protocol: RSC uses "Flight," a text format that can describe UI trees and resolve references to Client Bundles.
- The Wall: You can only pass things that can be turned into text (JSON + specialized React types).
- No Functions: Closures cannot cross the network boundary. Server Actions work via references, not serialization.
- 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 }}.