@lazarv/react-server
@virtual-frame/react-server provides first-class @lazarv/react-server integration with server rendering. The remote page is fetched during SSR and embedded in the response — the user sees styled content on first paint with zero layout shift, and the client resumes live updates without an extra network request.
Installation
npm install virtual-frame @virtual-frame/react-server @virtual-frame/storeServer Component
In a Server Component, VirtualFrame is an async component that fetches the remote page and streams it into the response.
// app/page.tsx
import { VirtualFrame } from "@virtual-frame/react-server";
export default async function Page() {
return <VirtualFrame src="http://remote:3003" />;
}Selector Projection
<VirtualFrame src="http://remote:3003" selector="#counter-card" />Multiple Projections from One Fetch
import { fetchVirtualFrame, VirtualFrame } from "@virtual-frame/react-server";
export default async function Page() {
const frame = await fetchVirtualFrame("http://remote:3003");
return (
<>
<VirtualFrame frame={frame} />
<VirtualFrame frame={frame} selector="#counter-card" />
</>
);
}See the Shared Store section below for host + remote bridge wiring, including the RSC-specific VirtualFrameStoreProvider context option.
Remote Side
The remote is a normal @lazarv/react-server app. See the Shared Store section below for how to read and write the bridged store from a "use client" component.
Shared Store
A shared store keeps state in sync between the host app and the remote app (including every projected frame) over a MessagePort bridge. Writes on either side propagate to the other automatically, and every useStore(...) subscription re-renders when the underlying value changes.
The store is a client-only object — it cannot cross the RSC serialisation boundary. The host creates it inside a "use client" component; the remote connects to it at runtime via the hidden iframe VirtualFrame mounts. You do not duplicate the store on the remote: the remote-side useStore() returns a proxy that forwards reads and writes across the port.
1. Create the store on the host
// app/store.ts
import { createStore } from "@virtual-frame/store";
export const store = createStore();
store.count = 0;createStore() returns a plain reactive object. Assign initial values directly — nested objects and arrays are supported. Paths are addressed as string arrays: ["count"], ["user", "name"], ["items", 0].
2. Pass the store to <VirtualFrame> on the host
Fetch the frame props in a Server Component and hand them to a "use client" wrapper that owns the store:
// app/page.tsx (Server Component)
import { fetchVirtualFrame } from "@virtual-frame/react-server";
import { prepareVirtualFrameProps } from "@virtual-frame/react-server/cache";
import { HostFrames } from "./components/HostFrames";
export default async function Page() {
const frame = await fetchVirtualFrame("http://remote:3003");
return (
<HostFrames
fullPage={await prepareVirtualFrameProps(frame)}
counterCard={await prepareVirtualFrameProps(frame, {
selector: "#counter-card",
})}
/>
);
}// app/components/HostFrames.tsx
"use client";
import { VirtualFrame } from "@virtual-frame/react-server";
import { useStore } from "@virtual-frame/react";
import { store } from "../store";
export function HostFrames({ fullPage, counterCard }) {
// Subscribe to a path — returns the current value, re-renders on change.
const count = useStore<number>(store, ["count"]);
return (
<>
<p>Host count: {count ?? 0}</p>
<button onClick={() => (store.count = (count ?? 0) + 1)}>Increment from host</button>
<button onClick={() => (store.count = 0)}>Reset</button>
{/* Any VirtualFrame that receives store= joins the same sync bridge. */}
<VirtualFrame {...fullPage} store={store} />
<VirtualFrame {...counterCard} store={store} />
</>
);
}TIP
prepareVirtualFrameProps is async — don't forget the await! Without it you'll spread a Promise object instead of the actual props, and mirroring will silently fail.
- Host reads/writes are direct:
store.countoperates on the host's in-memory object — no serialisation, no round-trip. - Passing
store={store}wires up the bridge: when the hidden iframe loads and the remote signalsvf-store:ready, the component opens aMessageChannel, transfers one port to the iframe, and callsconnectPort()on the host side. Multiple<VirtualFrame>instances sharing the samesrcshare one iframe and one port — the store is bridged exactly once.
Alternative: store via context
As an alternative to prop-drilling store={store} to every <VirtualFrame>, wrap the subtree in VirtualFrameStoreProvider. This lets you render <VirtualFrame> directly from a Server Component while the store stays client-only:
// app/components/StoreProvider.tsx
"use client";
import { VirtualFrameStoreProvider } from "@virtual-frame/react-server";
import { store } from "../store";
export function StoreProvider({ children }: { children: React.ReactNode }) {
return <VirtualFrameStoreProvider store={store}>{children}</VirtualFrameStoreProvider>;
}// app/page.tsx (Server Component)
import { VirtualFrame } from "@virtual-frame/react-server";
import { StoreProvider } from "./components/StoreProvider";
export default async function Page() {
return (
<StoreProvider>
<VirtualFrame src="http://remote:3003" />
<VirtualFrame src="http://remote:3003" selector="#counter-card" />
</StoreProvider>
);
}3. Consume the store on the remote
On the remote, use useStore from @virtual-frame/react-server in a "use client" component. It's a two-mode hook — no args returns the singleton StoreProxy, a path returns a reactive value:
"use client";
import { useStore } from "@virtual-frame/react-server";
function Counter() {
const store = useStore(); // StoreProxy singleton
const count = useStore<number>(["count"]); // reactive value at path
return <button onClick={() => (store.count = (count ?? 0) + 1)}>Count: {count ?? 0}</button>;
}| Call | Returns | Purpose |
|---|---|---|
useStore() | StoreProxy | Remote singleton. Connects to the host store on first call. |
useStore(["count"]) | T | Reactive subscription. Re-renders the component on value change. |
Standalone fallback
When the remote page is loaded directly in the browser (not through a VirtualFrame), there is no host and no port. In that case useStore() returns a plain in-memory store, so the page still works as a standalone @lazarv/react-server app. Writes stay local; reads return whatever was last written.
Tips
- Initialise on the host, not the remote. The host's values are the source of truth on first connect. Anything the remote writes before the port is open is kept local until the bridge finishes handshaking.
- Keep values serialisable. Values cross a
postMessageboundary — prefer plain objects, arrays, primitives. No class instances, functions, or DOM nodes. - Namespace per feature. For multiple features in one app, group keys under stable prefixes (
["cart", "items"],["auth", "user"]). - One store per remote URL is typical. Pass the same
storeto every frame that targets the same remote.
How Server Rendering Works
API Reference
<VirtualFrame>
Works in Server Components and Client Components.
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | --- | Remote URL to fetch and project |
frame | VirtualFrameResult | --- | Pre-fetched result (Server Component only) |
selector | string | --- | CSS selector for partial projection |
isolate | "open" | "closed" | "open" | Shadow DOM mode |
streamingFps | number | Record<string, number> | --- | Canvas/video streaming FPS |
store | StoreProxy | --- | Shared store for cross-frame state sync |
proxy | string | --- | Same-origin proxy prefix for client-side navigation |
ref | React.Ref | --- | Exposes { refresh() } |
<VirtualFrameStoreProvider>
Provides a store to all descendant VirtualFrame components via React context.
| Prop | Type | Description |
|---|---|---|
store | StoreProxy | Store instance from createStore() |
children | React.ReactNode | Child components |
useStore(selector?)
Remote-side hook. Returns the store instance or a reactive value at a path.
const store = useStore(); // store instance
const count = useStore<number>(["count"]); // reactive valuefetchVirtualFrame(url, options?)
Fetches a remote page and produces a server render result.
prepareVirtualFrameProps(frame, options?)
Converts a server render result into serialisable props for <VirtualFrame>. Returns a Promise — always await it.
| Option | Type | Default | Description |
|---|---|---|---|
selector | string | --- | CSS selector for partial projection |
isolate | "open" | "closed" | "open" | Shadow DOM mode |
proxy | string | --- | Same-origin proxy prefix for client-side navigation |
Examples
- react-server example ---
pnpm example:react-server