@lazarv/rsc
A bundler-agnostic, environment-agnostic React Server Components (RSC) serialization and deserialization library built on React's Flight protocol. Not a framework — a universal data transport layer.
This package provides a standalone implementation of the Flight protocol without any direct dependency on the react package and without bundler-specific globals. It is part of the @lazarv/react-server project.
Why
React's official react-server-dom-webpack package is tightly coupled to Webpack manifests and Node.js APIs. @lazarv/rsc removes both constraints:
- Bundler-agnostic — no Webpack plugin, no Vite plugin, no bundler manifests. Consumers wire up their own
moduleResolver/moduleLoaderinterfaces. - Environment-agnostic — built on Web Platform APIs (
ReadableStream,WritableStream,TextEncoder,FormData,Blob,URL, …). The same code runs in Node.js, Deno, Bun, Cloudflare Workers, the browser, or any runtime that supports the Web Platform. - No direct React imports — uses
Symbol.for()to access React internals, so it works with any compatible React version. - Full Flight protocol parity — Elements, Promises, Map, Set, Date, BigInt, RegExp, Symbol, URL, URLSearchParams, FormData, TypedArrays, ArrayBuffer, DataView, Blob, ReadableStream, async iterables, client/server references, Suspense, Fragment, lazy, memo, forwardRef, context, Activity, ViewTransition, and more.
How @lazarv/react-server uses it
@lazarv/react-server is a Vite-based React Server Components framework. It currently uses @lazarv/rsc for:
- Logger proxy — serializing structured log data across environment boundaries using the Flight protocol.
- Caching / cache providers — saving and restoring UI or data snapshots in any storage backend via RSC serialization.
With planned expansion to full cross-environment usage (worker threads, edge runtimes, cross-process communication) leveraging the environment-agnostic design of this package.
By extracting the Flight protocol into a standalone package, any tool or framework can adopt RSC serialization without buying into a specific bundler or runtime.
Use Cases
| Direction | Example |
|---|---|
| Server → Client | Streaming serialized React trees or structured data |
| Client → Server | Sending action arguments, form data, console logs in RSC format |
| Worker threads | Passing serialized React trees between threads |
| Cache providers | Saving/restoring UI or data snapshots in any storage backend |
| Cross-process | Piping RSC payloads between server processes |
| Any ↔ Any | Browser ↔ Server ↔ Worker ↔ Edge ↔ Cache |
Installation
npm install @lazarv/rsc
# or
pnpm add @lazarv/rsc
# or
yarn add @lazarv/rsc
Peer dependency: react >=19.0.0 (or >=0.0.0-experimental).
Entry Points
Two universal entry points — the same code runs everywhere that supports Web Platform APIs:
| Entry | Purpose |
|---|---|
@lazarv/rsc/server |
Serialization — render, register references, decode replies |
@lazarv/rsc/client |
Deserialization — consume streams, encode replies, call actions |
There are no platform-specific sub-entries. No
/server.node,/server.edge,/client.browser, etc.
Usage
Server-side (Serialization)
import {
renderToReadableStream,
registerServerReference,
registerClientReference,
createClientModuleProxy,
createTemporaryReferenceSet,
decodeReply,
decodeAction,
decodeFormState,
decodeReplyFromAsyncIterable,
} from "@lazarv/rsc/server";
Module Resolver
Provide a module resolver that tells the serializer how to resolve "use client" / "use server" references:
const stream = renderToReadableStream(element, {
moduleResolver: {
resolveClientReference(reference) {
return {
id: reference.$$id,
name: reference.$$name,
chunks: [/* chunk IDs to preload */],
};
},
resolveServerReference(reference) {
return {
id: reference.$$id,
name: reference.$$name,
};
},
},
onError(error) {
console.error("RSC Error:", error);
return error.digest; // returned as error.digest on the client
},
});
Registering References
// Register a client component
const ClientComponent = registerClientReference(
{}, // proxy object
"./ClientComponent", // module ID
"default" // export name
);
// Register a server action
async function submitForm(formData) {
"use server";
}
registerServerReference(submitForm, "action:submitForm", "submitForm");
// Create a full module proxy (all named exports become client references)
const clientModule = createClientModuleProxy("./MyClientModule");
Decoding Client Replies
// Decode a reply from the client (FormData or string body)
const args = await decodeReply(body, {
moduleLoader: {
loadServerAction: (id) => actionRegistry.get(id),
},
});
// Decode from a streaming async iterable
const args = await decodeReplyFromAsyncIterable(requestBodyStream, {
moduleLoader: {
loadServerAction: (id) => actionRegistry.get(id),
},
});
// Decode a form action (returns the action function)
const action = await decodeAction(formData, { moduleLoader });
// Decode form state for progressive enhancement
const state = await decodeFormState(actionResult, formData);
Client-side (Deserialization)
import {
createFromReadableStream,
createFromFetch,
encodeReply,
createServerReference,
createTemporaryReferenceSet,
} from "@lazarv/rsc/client";
Module Loader
Provide a module loader that tells the deserializer how to load client modules:
const moduleLoader = {
requireModule(metadata) {
// Synchronously return the module export
return moduleCache.get(metadata.id)?.[metadata.name];
},
preloadModule(metadata) {
// Optional: preload module chunks ahead of time
return import(metadata.id);
},
};
Consuming Flight Streams
// From a ReadableStream — returns a synchronous thenable
// compatible with React's use() protocol
const result = createFromReadableStream(stream, {
moduleLoader,
callServer: async (id, args) => {
const response = await fetch("/action", {
method: "POST",
body: await encodeReply(args),
});
return createFromFetch(response, { moduleLoader, callServer });
},
});
// result.status === "pending" | "fulfilled" | "rejected"
// result.value is available synchronously once fulfilled
// From a fetch response
const result = createFromFetch(fetch("/rsc"), { moduleLoader, callServer });
Server Action References
// Create a callable server action proxy
const myAction = createServerReference("action:myAction", callServer);
// Call it — args are encoded and sent via callServer
await myAction(arg1, arg2);
// .bind() works for partial application
const boundAction = myAction.bind(null, boundArg);
await boundAction(remainingArg);
Encoding Replies
// Encode arguments for a server action call
// Returns string or FormData depending on content
const encoded = await encodeReply([arg1, arg2]);
Temporary References
Temporary references allow non-serializable values (functions, React elements, class instances, local symbols) to survive a round-trip:
// Client-side: create a Map-based temp ref set
import { createTemporaryReferenceSet } from "@lazarv/rsc/client";
const tempRefs = createTemporaryReferenceSet();
// Encode with temp refs — non-serializable values are stored in the Map
const encoded = await encodeReply(args, { temporaryReferences: tempRefs });
// Later, recover values when consuming a response
const result = createFromReadableStream(responseStream, {
moduleLoader,
temporaryReferences: tempRefs,
});
// Server-side: create a WeakMap-based temp ref set
import { createTemporaryReferenceSet } from "@lazarv/rsc/server";
const tempRefs = createTemporaryReferenceSet();
// Decode with temp refs
const args = await decodeReply(body, { temporaryReferences: tempRefs });
// Render with the same temp refs — proxies resolve back to $T references
const stream = renderToReadableStream(element, {
temporaryReferences: tempRefs,
});
API Reference
Server API (@lazarv/rsc/server)
| Export | Description |
|---|---|
renderToReadableStream(model, options?) |
Serialize a React element tree to a Flight ReadableStream |
decodeReply(body, options?) |
Decode a FormData or string reply from the client |
decodeReplyFromAsyncIterable(iterable, options?) |
Decode a reply from a streaming AsyncIterable<Uint8Array> |
decodeAction(body, options?) |
Decode a server action invocation from FormData |
decodeFormState(result, body) |
Decode form state for progressive enhancement |
registerServerReference(fn, id, name) |
Register a function as a server reference ("use server") |
registerClientReference(proxy, id, name) |
Register an object as a client reference ("use client") |
createClientModuleProxy(moduleId) |
Create a Proxy where every property access returns a client reference |
createTemporaryReferenceSet() |
Create a WeakMap for temporary reference tracking |
prerender(model, options?) |
Prerender a model to a static prelude ReadableStream (waits for all async work) |
Client API (@lazarv/rsc/client)
| Export | Description |
|---|---|
createFromReadableStream(stream, options?) |
Deserialize a Flight stream into a React element tree (synchronous thenable) |
createFromFetch(responsePromise, options?) |
Deserialize a fetch() response into a React element tree |
encodeReply(value, options?) |
Encode a value for sending to the server (string or FormData) |
createServerReference(id, callServer) |
Create a callable proxy for a server action |
createTemporaryReferenceSet() |
Create a Map for temporary reference tracking |
Types
Full type definitions are in types.d.ts. Key interfaces:
interface ModuleResolver {
resolveClientReference?(reference: unknown): ClientReferenceMetadata | null;
resolveServerReference?(reference: unknown): ServerReferenceMetadata | null;
}
interface ModuleLoader {
preloadModule?(metadata: ClientReferenceMetadata): Promise<void> | void;
requireModule(metadata: ClientReferenceMetadata): unknown;
loadServerAction?(id: string): Promise<Function> | Function;
}
interface ClientReferenceMetadata {
id: string;
name: string;
chunks?: string[];
}
interface ServerReferenceMetadata {
id: string;
bound?: boolean;
}
interface RenderToReadableStreamOptions {
moduleResolver?: ModuleResolver;
onError?: (error: unknown) => string | void;
identifierPrefix?: string;
temporaryReferences?: Map<string, unknown>;
environmentName?: string;
filterStackFrame?: (sourceURL: string, functionName: string) => boolean;
signal?: AbortSignal;
}
interface CreateFromReadableStreamOptions {
moduleLoader?: ModuleLoader;
callServer?: (id: string, args: unknown[]) => Promise<unknown>;
temporaryReferences?: Map<string, unknown>;
typeRegistry?: Record<string, new (buffer: ArrayBuffer) => ArrayBufferView>;
}
Serialization Coverage
All types supported by React's Flight protocol are implemented:
| Category | Types |
|---|---|
| Primitives | string, number, boolean, null, undefined, BigInt, Symbol (global) |
| Objects | Date, RegExp, URL, URLSearchParams, Map, Set, Error |
| Binary | ArrayBuffer, Int8Array, Uint8Array, Float32Array, DataView, Blob, … |
| Streams | ReadableStream, AsyncIterable |
| Form data | FormData |
| React | Elements, Fragments, Suspense, Lazy, Memo, ForwardRef, Context, Activity, ViewTransition |
| RSC | Client references ("use client"), Server references ("use server"), bound actions (.bind()) |
| Special | Promises, Thenables, Temporary references, Error digest propagation |
Design Decisions
Web standards only
This library targets the Web Platform API surface (ReadableStream, WritableStream, TextEncoder, FormData, Blob, URL, …). No Node.js-specific primitives — no stream.Readable, AsyncLocalStorage, or Buffer. A single entry point per side runs in any environment.
Abstract module loader — no bundler coupling
Only abstract moduleResolver / moduleLoader interfaces are supported. No Webpack plugin, no Vite plugin, no bundler-specific manifest generation. Consumers wire up their own resolution logic, which makes this library usable with any bundler or runtime.
No runtime-specific hooks
No Node.js ESM loader hooks, no CJS require hooks, no environment-specific registration. The consumer is responsible for handling "use client" / "use server" directive detection in their build tool or runtime.
Comparison with react-server-dom-webpack
| Capability | react-server-dom-webpack |
@lazarv/rsc |
|---|---|---|
| Flight protocol | ✅ | ✅ Full parity |
| Bundler | Webpack only | Any (abstract interface) |
| Runtime | Node.js (+ browser client) | Any Web Platform runtime |
renderToReadableStream |
✅ | ✅ |
renderToPipeableStream |
✅ (Node.js) | — (use ReadableStream) |
createFromReadableStream |
✅ | ✅ |
createFromNodeStream |
✅ (Node.js) | — (use ReadableStream) |
encodeReply / decodeReply |
✅ | ✅ |
| Temporary references | ✅ | ✅ |
Bound actions (.bind()) |
✅ | ✅ |
| Error digest propagation | ✅ | ✅ |
Synchronous thenable (use()) |
✅ | ✅ |
| Webpack plugin / manifest | ✅ | — (by design) |
| Node ESM loader hooks | ✅ | — (by design) |
react-server condition gating |
✅ | — |
| Prerender | ✅ | ✅ |
Related
@lazarv/react-server— The Vite-based React Server Components framework that uses this package- react-server.dev — Documentation and guides
- React Flight protocol — The upstream React implementation
License
MIT