Skip to main content

React

@webview-ts/react wraps the bridge in a provider and hooks. Everything is typed from the plugins you pass to createBridgeReact.

Setup

bridge.ts
import { createBridgeReact } from '@webview-ts/react';
import { camera, location } from './plugins';

export const { BridgeProvider, useBridge, usePlugin, useAction, useEvent } = createBridgeReact({
plugins: [camera, location],
config: { timeout: 10_000 },
interceptors: { request: [authInterceptor] },
});
main.tsx
<BridgeProvider>
<App />
</BridgeProvider>

Strict Mode is fully supported — the provider separates instance creation (useMemo) from side effects (useEffect), so double-invocation never leaks listeners or duplicates DevTools connections.

usePlugin — one handle per action

const { takePhoto } = usePlugin(camera);

takePhoto.execute({ quality: 0.9 }); // typed payload → typed Promise
takePhoto.status; // 'idle' | 'loading' | 'success' | 'error'
takePhoto.data; // response | null
takePhoto.error; // BridgeCallError | null
takePhoto.isLoading; // boolean
takePhoto.reset(); // reset state + invalidate this action's cache

Plus a typed event subscriber using short names:

const { on } = usePlugin(location);
useEffect(() => on('updated', (pos) => setPosition(pos)), []);

useAction — one action by full name

const info = useAction('device.getInfo', { cache: 60_000 });

Action names are exact — an undeclared name is a compile error.

useEvent — subscribe to one event

useEvent('location.updated', (pos) => setPosition(pos));

Subscription lifetime is tied to the component; unsubscription is automatic.

useBridge — the escape hatch

const { call, on, off, isAvailable, connectionMode, bridge } = useBridge();

call keeps the same key/payload/response inference as usePlugin. bridge is the underlying BridgeClient for advanced usage (global interceptors, onCall telemetry).

The host role — a React page as an iframe shell

The web is always both. The same React app that is a client inside a WebView can be the host of embedded iframes. useBridgeHost wraps the neutral core factory; you inject the transport:

import { useBridgeHost, IframeHostAdapter, defineHandlers } from '@webview-ts/react';
import { shell } from './plugins';

function Shell({ frameRef }: { frameRef: HTMLIFrameElement }) {
const { sendEvent } = useBridgeHost({
adapter: new IframeHostAdapter(frameRef, CHILD_ORIGIN),
plugins: [shell.host({ getUser: async () => user, showToast: async (p) => toast(p) })],
});

return <button onClick={() => sendEvent('shell.themeChanged', { theme: 'dark' })}></button>;
}

Strict Mode is handled the same way as the client side — setup→cleanup→setup re-attaches cleanly. See Iframe embeds for the full pattern including multi-frame routing.

Custom actions without plugins

type CustomActions = {
'app.custom': { payload: { id: string }; response: { done: boolean } };
};

const { useAction } = createBridgeReact<CustomActions>({});