React Integration
ConsoleHostProvider and ConsoleIframe for React host apps.
pnpm add @riftix/console-bridge-hostThe React adapter lives at @riftix/console-bridge-host/react.
ConsoleHostProvider
Wraps your app (or a subtree) and makes the ConsoleHost instance available via context.
import { ConsoleHostProvider } from "@riftix/console-bridge-host/react";
export function Shell({ children }: { children: React.ReactNode }) {
return (
<ConsoleHostProvider
defaultContextProvider={() => ({ theme: "light", locale: "en-US" })}
defaultAuthorizationProvider={(channel) => ({
clientId: channel.productId,
organizationId: "org_123",
token: "your-jwt-here",
expiresAt: Date.now() + 3_600_000,
})}
>
{children}
</ConsoleHostProvider>
);
}Props
interface ConsoleHostProviderProps extends ConsoleHostOptions {
children: ReactNode;
/**
* Provide a pre-constructed ConsoleHost instance.
* If omitted, the provider creates and manages one internally.
*/
host?: ConsoleHost;
}Accepts all ConsoleHostOptions (defaultContextProvider, defaultAuthorizationProvider, defaultCapabilities) as props in addition to the optional host override.
StrictMode safe: the host instance is created inside useEffect so React's double-invocation in development mode does not leave a disposed instance in the tree. The provider renders null until the effect has run.
useConsoleHost
Access the ConsoleHost instance anywhere inside the provider.
import { useConsoleHost } from "@riftix/console-bridge-host/react";
function ToastHandler() {
const host = useConsoleHost(); // throws if called outside ConsoleHostProvider
// ...
}Throws "useConsoleHost must be used inside <ConsoleHostProvider>" if no provider is found in the tree.
ConsoleIframe
A thin wrapper around <iframe> that registers and unregisters the channel automatically.
import { ConsoleIframe } from "@riftix/console-bridge-host/react";
export function BillingPage() {
return (
<ConsoleIframe
src="https://billing.example.com"
origin="https://billing.example.com"
productId="billing"
style={{ width: "100%", height: "100%", border: "none" }}
/>
);
}Props
interface ConsoleIframeProps
extends Omit<IframeHTMLAttributes<HTMLIFrameElement>, "src"> {
src: string;
origin: string;
productId: string;
contextProvider?: RegisterIframeOptions["contextProvider"];
authorizationProvider?: RegisterIframeOptions["authorizationProvider"];
handlers?: RegisterIframeOptions["handlers"];
capabilities?: readonly string[];
/** Called with the IframeChannel after registration. */
onChannel?: (channel: IframeChannel) => void;
/**
* Called whenever the iframe reports a URL change via navigation.locationChanged.
* Use this to mirror the iframe path into the host URL bar.
* `path` is the iframe-relative path (e.g. "/invoices/123").
*/
onLocation?: (path: string, channel: IframeChannel) => void;
}Re-registers the channel whenever origin, productId, contextProvider, authorizationProvider, handlers, or capabilities change. Accepts a forwarded ref for the underlying <iframe> element.
URL sync pattern
The canonical pattern for mirroring iframe navigation into the host URL bar — without reloading the iframe:
import { useCallback } from "react";
import { createFileRoute } from "@tanstack/react-router";
import { ConsoleIframe } from "@riftix/console-bridge-host/react";
import type { IframeChannel } from "@riftix/console-bridge-host";
const CLIENT_ORIGIN = "https://billing.example.com";
// Catch-all route: /billing and /billing/*
export const Route = createFileRoute("/billing/$")({
component: BillingPage,
});
function BillingPage() {
const { _splat } = Route.useParams();
const subPath = _splat ? `/${_splat}` : "/";
const handleLocation = useCallback((path: string, _channel: IframeChannel) => {
const hostPath = path === "/" ? "/billing" : `/billing${path}`;
if (window.location.pathname !== hostPath) {
// Use replaceState (not router.navigate) — no iframe reload
window.history.replaceState(null, "", hostPath);
}
}, []);
return (
<ConsoleIframe
src={`${CLIENT_ORIGIN}${subPath}`}
origin={CLIENT_ORIGIN}
productId="billing"
onLocation={handleLocation}
style={{ width: "100%", height: "100%", border: "none" }}
/>
);
}Deep links work automatically: when the user opens /billing/invoices/123, _splat is "invoices/123" and the iframe src becomes https://billing.example.com/invoices/123.
RPC handlers
Register global handlers with useConsoleHost() + host.handle():
import { useEffect } from "react";
import { useConsoleHost } from "@riftix/console-bridge-host/react";
import { toast } from "sonner";
export function RpcHandlers() {
const host = useConsoleHost();
useEffect(() => {
const cleanup = [
host.handle("toast.show", ({ message, variant }) => {
toast[variant ?? "info"](message);
}),
host.handle("modal.confirm", ({ title, body, confirmLabel, cancelLabel, destructive }) => {
// Return a Promise<boolean> resolved by your modal component
return openConfirmDialog({ title, body, confirmLabel, cancelLabel, destructive });
}),
];
return () => cleanup.forEach((fn) => fn());
}, [host]);
return null;
}Breadcrumbs
Subscribe to breadcrumb changes from registered channels:
import { useState, useEffect } from "react";
import { useConsoleHost } from "@riftix/console-bridge-host/react";
import type { BridgeBreadcrumb } from "@riftix/console-bridge-types";
export function useBreadcrumbs(productId: string) {
const host = useConsoleHost();
const [crumbs, setCrumbs] = useState<ReadonlyArray<BridgeBreadcrumb>>([]);
useEffect(() => {
const unsubs: Array<() => void> = [];
const unregister = host.onChannelRegistered((channel) => {
if (channel.productId !== productId) return;
const unsub = channel.onBreadcrumbsChanged(setCrumbs);
unsubs.push(unsub);
});
return () => {
unregister();
unsubs.forEach((fn) => fn());
};
}, [host, productId]);
return crumbs;
}Auth rotation
Re-issue authorization tokens before they expire:
useEffect(() => {
const id = setInterval(() => {
host.setAuthorizationForProduct("billing", {
clientId: "billing",
organizationId: "org_123",
token: refreshToken(),
expiresAt: Date.now() + 3_600_000,
});
}, 30_000);
return () => clearInterval(id);
}, [host]);