Skip to content

@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

sh
npm install virtual-frame @virtual-frame/react-server @virtual-frame/store

Server Component

In a Server Component, VirtualFrame is an async component that fetches the remote page and streams it into the response.

tsx
// app/page.tsx
import { VirtualFrame } from "@virtual-frame/react-server";

export default async function Page() {
  return <VirtualFrame src="http://remote:3003" />;
}

Selector Projection

tsx
<VirtualFrame src="http://remote:3003" selector="#counter-card" />

Multiple Projections from One Fetch

tsx
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

ts
// 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:

tsx
// 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",
      })}
    />
  );
}
tsx
// 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.count operates 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 signals vf-store:ready, the component opens a MessageChannel, transfers one port to the iframe, and calls connectPort() on the host side. Multiple <VirtualFrame> instances sharing the same src share 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:

tsx
// 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>;
}
tsx
// 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:

tsx
"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>;
}
CallReturnsPurpose
useStore()StoreProxyRemote singleton. Connects to the host store on first call.
useStore(["count"])TReactive 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 postMessage boundary — 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 store to every frame that targets the same remote.

How Server Rendering Works

Server① Fetch remote pageDownload and parse HTML② Extract styles + bodyPrepare styles and content③ Render to responseStyled content, zero JS neededServer OutputFully styled content embedded in the page — visible on first paintEverything needed for seamless client activation is includedHTML responseClient④ ResumeActivate from server outputNo extra network requests⑤ Live projectionContent stays in syncFull interactivity enabled⑥ Shared resourcesMultiple projectionsOne source, many viewsBenefits✓ Instant paint — content visible before JS runs✓ No flash of unstyled content✓ No extra network requests on the client✓ Minimal data sent to the browserStore bridge (optional)Pass a store to VirtualFrameState syncs automaticallyChanges propagate in both directionsWorks across host and remote

API Reference

<VirtualFrame>

Works in Server Components and Client Components.

PropTypeDefaultDescription
srcstring---Remote URL to fetch and project
frameVirtualFrameResult---Pre-fetched result (Server Component only)
selectorstring---CSS selector for partial projection
isolate"open" | "closed""open"Shadow DOM mode
streamingFpsnumber | Record<string, number>---Canvas/video streaming FPS
storeStoreProxy---Shared store for cross-frame state sync
proxystring---Same-origin proxy prefix for client-side navigation
refReact.Ref---Exposes { refresh() }

<VirtualFrameStoreProvider>

Provides a store to all descendant VirtualFrame components via React context.

PropTypeDescription
storeStoreProxyStore instance from createStore()
childrenReact.ReactNodeChild components

useStore(selector?)

Remote-side hook. Returns the store instance or a reactive value at a path.

tsx
const store = useStore(); // store instance
const count = useStore<number>(["count"]); // reactive value

fetchVirtualFrame(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.

OptionTypeDefaultDescription
selectorstring---CSS selector for partial projection
isolate"open" | "closed""open"Shadow DOM mode
proxystring---Same-origin proxy prefix for client-side navigation

Examples