frame-bridge
@mhanzelka/react-frame-bridge

react-frame-bridge

React provider and hooks. Manages bridge lifecycle — opens on mount, closes on unmount.

Peer dependencies: react ≥ 19.0.0, react-dom ≥ 19.0.0, @mhanzelka/frame-bridge (installed separately).

BridgeProvider

Creates and owns the bridge instance. The bridge is created once on mount — changes tochannelName, role, or enabledTransports after mount are ignored. Remount the provider to apply new configuration.

import { BridgeProvider, IframeBridgeHost, useBridge } from "@mhanzelka/react-frame-bridge";

type Messages = { type: "ping" | "pong"; value: number };

// Parent page
function Parent() {
    return (
        <BridgeProvider<Messages>
            open={true}
            channelName="my-channel"
            role="parent"
            targetOrigin="https://child.example.com"
            enabledTransports={["post-message-channel"]}
        >
            <IframeBridgeHost
                src="https://child.example.com/app"
                targetOrigin="https://child.example.com"
                style={{ width: "100%", height: 500 }}
            />
            <Controls />
        </BridgeProvider>
    );
}

// Child iframe at child.example.com
function Child() {
    return (
        <BridgeProvider<Messages>
            open={true}
            channelName="my-channel"
            role="child"
            targetOrigin="https://parent.example.com"
            enabledTransports={["post-message-channel"]}
        >
            <Handler />
        </BridgeProvider>
    );
}
Prop / MethodTypeDefaultDescription
openbooleanSet to false to keep the bridge closed.
channelNamestringMust match on both sides.
role"parent" | "child"Role of this side.
enabledTransportsTransportType[]["post-message-channel"]Which transports to use.
targetOriginstringRequired for "post-message-channel".
idstringExplicit bridge instance id used as-is — replaces the random one. Useful for naming endpoints so peers can address each other deterministically via send({ targetId }). Caller is responsible for uniqueness within the same channelName. Ignores prefix when set.
prefixstringOptional prefix for the generated bridge ID. Ignored when id is set.
options.observer(event) => voidCalled for every message sent/received. Event carries direction: "in" | "out" and transportType for filtering/logging.
bridgeOptionsCreateBridgeOptionsAdvanced options passed to createBridge.

Named endpoints

Pass id to give each provider a deterministic name. Peers then address each other via send(data, { targetId }) without a discovery handshake. Mostly relevant on broadcast-channel with three or more peers — see the Targeted addressing section in the core docs for details.

// Each side sets its own deterministic id. Peers can then address each other
// directly via send({ targetId }) — no discovery handshake needed.
function Lobby() {
    return (
        <BridgeProvider<Messages>
            id="lobby"
            open={true}
            channelName="rooms"
            role="parent"
            enabledTransports={["broadcast-channel"]}
        >
            <Controls />
        </BridgeProvider>
    );
}

function Controls() {
    const bridge = useBridge<Messages>();
    const ping = () =>
        bridge.send({ type: "ping", value: 1 }, { targetId: "room-42" });
    return <button onClick={ping}>Ping room-42</button>;
}

useBridge<T>()

Returns the Bridge instance from the nearest BridgeProvider. Throws if used outside a provider.

function Controls() {
    const bridge = useBridge<Messages>();

    const ping = async () => {
        const reply = await bridge.send({ type: "ping", value: 1 });
        console.log(reply); // { type: "pong", value: 2 }
    };

    return <button onClick={ping}>Ping</button>;
}

Handling incoming messages

Use bridge.onMessage() inside a useEffect. The return value is the unsubscribe function.

function Handler() {
    const bridge = useBridge<Messages>();

    useEffect(() => {
        return bridge.onMessage(async (msg) => {
            if (msg.type === "ping")
                return { type: "pong", value: msg.value + 1 };
        });
    }, [bridge]);

    return null;
}

useBridgeState()

Subscribes to bridge state changes via useSyncExternalStore. Re-renders on every state update.

function StatusBar() {
    const state = useBridgeState();

    return (
        <div>
            {state.state === "open" && <span>● Connected</span>}
            {state.state === "closed" && <span>● Disconnected</span>}
            <span>Pending: {state.pendingCount}</span>
        </div>
    );
}

// state shape:
// state.state          → "open" | "closed" | "partially-open"
// state.pendingCount   → number of unanswered requests
// state.transports     → { [type]: { state, messageCount } }

IframeBridgeHost

Drop-in <iframe> wrapper that automatically sets contentWindow as the postMessage target on the parent bridge.

Prop / MethodTypeDefaultDescription
srcstringiframe URL.
targetOriginstringOrigin to use for postMessage.
onChildReady(bridge: Bridge<T>) => voidFires after the iframe onLoad event AND the child bridge answers a sys ping. Use to send init payloads safely.
onChildReadyError(error: Error) => voidFires when waitForReady rejects (timeout, transport error). onChildReady will not fire.
readyTimeoutMsnumberOverride Bridge.waitForReady() default timeout (5000ms).
...restIframeHTMLAttributesAll standard iframe attributes.

Use onChildReady to send the first message only once the child bridge has confirmed it is alive — prevents lost init payloads when the iframe loads before the child mounts.

<IframeBridgeHost
    src="https://child.example.com/app"
    targetOrigin="https://child.example.com"
    readyTimeoutMs={3000}
    onChildReady={(bridge) => bridge.send({ type: "init", config })}
    onChildReadyError={(err) => console.warn("child failed to come up", err)}
/>

useBridgeWindow<T>()

Opens a popup window with a bridge connection. Tears down the bridge when the popup closes.

import { useBridgeWindow } from "@mhanzelka/react-frame-bridge";

function OpenPopupButton() {
    const { open } = useBridgeWindow<Messages>({
        onMessage: async (msg) => ({ ...msg, handled: true }),
        onBridgeReady: async (bridge) => {
            await bridge.send({ type: "init" });
        },
    });

    return (
        <button onClick={() => open({ url: "/popup", name: "settings" })}>
            Open Settings
        </button>
    );
}