Props Across the Boundary
You will learn how data travels from a server component to a client island, what values survive the trip, and what to do instead when something cannot travel.
A server component can render a client island and hand it data computed on the server. The server embeds those props in the page next to the island HTML. The browser reads them when it hydrates.
import { useState } from "preact/hooks";
export function App() {
return (
<main>
<h1>Props across the boundary</h1>
<Counter initial={5} />
</main>
);
}
function Counter({ initial }: { initial: number }) {
"use client";
const [count, setCount] = useState(initial);
return (
<button type="button" onClick={() => setCount((c) => c + 1)}>
Count: {count}
</button>
);
}
Here App runs only on the server. Counter renders there once, then hydrates in the browser starting from initial={5}. You wrote it like normal props. The framework handled the trip.
What you can pass
Think of this as richer than JSON. These values round trip intact:
- Primitives:
string,number(NaNand infinities round trip exactly, and-0stays negative),boolean,null,undefined,bigint. Symbol.for(key). A bareSymbol()throws because it has no shared key to look up.Date(invalid dates stay invalid),URLwith its href,RegExpwith source and flags.Error, redacted by default to a generic name and message with no stack.causealways serializes, so you still see why it failed.- Binary data:
ArrayBuffer, typed arrays, andDataView. Bytes stay bytes. MapandSet, including non string keys.- Arrays, where holes decode as
undefined. Other iterables like generators andargumentstravel as arrays. - Plain objects with enumerable string keyed entries. If an object defines
toJSON, you get its result. - Promises, which arrive as real promises and settle later without another round trip. Read Streaming and Suspense for how that timing works.
- Components and handlers behind a
use clientboundary, which reconnect to their browser code when the island hydrates. - Shared and cyclic values, where repeats and cycles come back as the same shared object.
If you are passing database rows, fetch results, dates, maps, or binary blobs, you can usually pass them directly. Resolve handles and streams first, then pass the data.
What you cannot pass
A few things cannot travel. They fail loudly so you notice during development:
- Plain functions with no
use clientboundary. They throw. Move the function into a"use client"module, or pass plain data instead. refs on elements. They skip with a warning and reattach when the island hydrates. Do not rely on them for first paint.- Symbol keyed properties. They drop with a warning.
- Server only resources like database handles and open streams. Resolve those on the server and pass the result.
If you see one of these errors, the fix is almost always the same. Do more work on the server, then pass a smaller result to the client.
A note on scoped functions
Scoped "use client" functions can capture outer values from the server scope. The client adds those captured values to the front of the props or call arguments. The same format rules apply, with one exception for events.
The event path writes captures directly into the listener script as plain JavaScript values. That keeps the script self contained, but it means captures there must have a literal form. Promises, non reference functions, and cycles are rejected with an explicit error. This is intentional. Dropping an argument would shift later arguments left and land the DOM event in the wrong parameter. Pass promises through a client component prop instead, where they resolve normally. Read Events for the full comparison.
The exact wire format is specified in preact-progressive/encoder. You rarely need it, but it is there when you want to see the bytes.