Bridge React Native
Bidirectional messaging between React Native and WebView. Code snippets on this page use syntax highlighting with the Dracula theme (Prism) in the generated documentation.
---
Table of contents
- 1. Introduction
- 2. Setup
- 2.1 WebView side
- 2.2 RN side - Adapter
- 3. Best practices
- 3.1 WebView: global bridge
- 3.2 RN: global adapter usage
- 4. Usage (API)
- 5. RN - Web flow
- 6. Implementation examples
- 6.1 Download with progress
- 6.2 Completed todos list
- 6.3 Native button - paint screen red
---
1. Introduction
The Bridge lets you use the same Fox Events API in your React Native app (via an adapter) and inside the WebView (via minimal setup). Events emitted in one environment are received in the other — commands, data, or lifecycle events (e.g. “web ready”, “reload”) without custom postMessage glue.
Set up the bridge once on the WebView side with createReactNativeBridge and on the RN side with createFoxRNAdapter and your WebView’s onMessage / inject. Then use fox.emit, fox.on, and fox.once in RN and Fox.emit / Fox.on in the web app.
---
2. Setup
2.1 WebView side
import { createReactNativeBridge } from "fox-events/bridge-react-native";
const dispose = createReactNativeBridge({
direction: "both",
filter: (name) => name.startsWith("app:"),
debug: false,
});
dispose();2.2 RN side — Adapter
import { useRef, useMemo } from "react";
import { WebView } from "react-native-webview";
import { createFoxRNAdapter } from "fox-events/bridge-react-native";
function App() {
const webViewRef = useRef<WebView>(null);
const fox = useMemo(
() =>
createFoxRNAdapter({
sendToWebView: (msg) => {
const code = `window.postMessage(${JSON.stringify(msg)}, '*'); true;`;
webViewRef.current?.injectJavaScript(code);
},
filter: (name) => name.startsWith("app:"),
}),
[]
);
return (
<WebView
ref={webViewRef}
source={{ uri: "https://example.com" }}
onMessage={(e) => fox.handleMessage(e.nativeEvent.data)}
/>
);
}---
3. Best practices
3.1 WebView: global bridge
Initialize the bridge once in a module imported at the entry point. Any component can use Fox.emit and Fox.on without repeating the setup.
Bridge module (src/bridge.ts):
import { createReactNativeBridge } from "fox-events/bridge-react-native";
createReactNativeBridge({
direction: "both",
filter: (name) => name.startsWith("app:"),
debug: false,
});Entry point (main.tsx): import before App:
import "./bridge";
import App from "./App";3.2 RN: global adapter usage
To use the same adapter across screens without passing props, register it globally.
Option A — Global module (app/fox-global.ts):
import type { FoxRNAdapter } from "fox-events/bridge-react-native";
let foxInstance: FoxRNAdapter | null = null;
export function setFox(fox: FoxRNAdapter): void {
foxInstance = fox;
}
export function getFox(): FoxRNAdapter {
if (foxInstance == null) {
throw new Error("Fox not initialized. Ensure the layout with WebView has mounted and called setFox(fox).");
}
return foxInstance;
}In the layout with WebView: call setFox(fox) after creating the adapter:
const fox = useMemo(() => createFoxRNAdapter({ sendToWebView: (msg) => { /* ... */ } }), []);
useEffect(() => {
setFox(fox);
}, [fox]);In any screen: use getFox():
import { getFox } from "@/app/fox-global";
export default function OtherScreen() {
const fox = getFox();
const handleSend = () => fox.emit("app:command", { id: "1" });
useEffect(() => {
const unsub = fox.on("app:response", (p) => console.log(p));
return () => unsub();
}, [fox]);
return (/* ... */);
}Option B — Hook that uses the global (app/fox-context.tsx):
import type { FoxRNAdapter } from "fox-events/bridge-react-native";
import { getFox } from "./fox-global";
export function useFox(): FoxRNAdapter {
return getFox();
}Usage: const fox = useFox();. To inject via the React tree, use createContext and <FoxContext.Provider value={fox}> in the layout.
---
4. Usage (API)
fox.emit("app:command", { action: "reload" });
fox.on("app:ready", (payload) => console.log(payload));
const payload = await fox.once("app:login");---
5. RN ↔ Web flow
- RN → Web:
fox.emit("app:command", payload)in RN; the bridge on the web callsFox.emit(name, payload); anyFox.on("app:command", ...)in the web runs. - Web → RN:
Fox.emit("app:ready", payload)in the web; the bridge sends to RN viaReactNativeWebView.postMessage; in RN,onMessagecallsfox.handleMessage(data); anyfox.on("app:ready", ...)in RN runs.
Example — Web:
import { Fox } from "fox-events";
import { createReactNativeBridge } from "fox-events/bridge-react-native";
createReactNativeBridge({ direction: "both", filter: (n) => n.startsWith("app:") });
Fox.on("app:command", (payload) => {
if (payload.action === "reload") window.location.reload();
});
Fox.emit("app:ready", { version: "1.0" });Example — React Native:
fox.emit("app:command", { action: "reload" });
fox.on("app:ready", (payload) => {
console.log("Web says ready:", payload.version);
});---
6. Implementation examples
6.1 Example: download with progress
Button in WebView triggers a “download”; RN simulates and sends progress; the web shows a bar and toast when done.
Events: app:download:start (web → RN), app:download:progress (RN → web), app:download:complete (RN → web).
Web:
import { useEffect, useState } from "react";
import { Fox } from "fox-events";
import { createReactNativeBridge } from "fox-events/bridge-react-native";
function App() {
const [downloadProgress, setDownloadProgress] = useState<number | null>(null);
const [toast, setToast] = useState<string | null>(null);
useEffect(() => {
const dispose = createReactNativeBridge({
direction: "both",
filter: (name) => name.startsWith("app:"),
debug: false,
});
Fox.on("app:download:progress", (payload: { progress: number }) => {
setDownloadProgress(payload.progress);
});
Fox.on("app:download:complete", () => {
setDownloadProgress(null);
setToast("Download complete!");
setTimeout(() => setToast(null), 3000);
});
return () => dispose();
}, []);
const handleDownload = () => {
setDownloadProgress(0);
Fox.emit("app:download:start", {});
};
return (
<div>
<button onClick={handleDownload} disabled={downloadProgress !== null}>
{downloadProgress !== null ? `Downloading... ${downloadProgress}%` : "Download"}
</button>
{downloadProgress !== null && downloadProgress < 100 && (
<div className="progress-bar" style={{ width: `${downloadProgress}%` }} />
)}
{toast && <div className="toast">{toast}</div>}
</div>
);
}React Native:
import { useRef, useMemo, useEffect } from "react";
import { WebView } from "react-native-webview";
import { createFoxRNAdapter } from "fox-events/bridge-react-native";
export default function App() {
const webViewRef = useRef<WebView>(null);
const downloadIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
const fox = useMemo(
() =>
createFoxRNAdapter({
sendToWebView: (msg) => {
const code = `window.postMessage(${JSON.stringify(msg)}, '*'); true;`;
webViewRef.current?.injectJavaScript(code);
},
filter: (name) => name.startsWith("app:"),
}),
[]
);
useEffect(() => {
fox.on("app:download:start", () => {
if (downloadIntervalRef.current) clearInterval(downloadIntervalRef.current);
let progress = 0;
downloadIntervalRef.current = setInterval(() => {
progress += Math.random() * 15 + 5;
if (progress >= 100) {
if (downloadIntervalRef.current) {
clearInterval(downloadIntervalRef.current);
downloadIntervalRef.current = null;
}
fox.emit("app:download:complete", {});
} else {
fox.emit("app:download:progress", { progress: Math.round(progress) });
}
}, 300);
});
return () => {
if (downloadIntervalRef.current) clearInterval(downloadIntervalRef.current);
};
}, [fox]);
const handleWebViewLoadEnd = () => {
fox.emit("app:mounted", { timestamp: Date.now() });
};
return (
<WebView
ref={webViewRef}
onLoadEnd={handleWebViewLoadEnd}
onMessage={(e) => fox.handleMessage(e.nativeEvent.data)}
source={{ uri: "https://your-webview-url.com" }}
/>
);
}Flow summary: 1) Web emits app:download:start. 2) RN simulates and emits app:download:progress. 3) Web updates bar. 4) RN emits app:download:complete. 5) Web shows toast.
---
6.2 Example: completed todos list
RN sends a list of completed todos on onLoadEnd; the web receives and renders it.
Event: app:todos:completed (RN → web), payload { todos: Array<{ id, title, completedAt }> }.
Web:
type TodoCompleted = { id: string; title: string; completedAt: number };
function App() {
const [completedTodos, setCompletedTodos] = useState<TodoCompleted[]>([]);
useEffect(() => {
const dispose = createReactNativeBridge({
direction: "both",
filter: (name) => name.startsWith("app:"),
});
Fox.on("app:todos:completed", (payload: { todos: TodoCompleted[] }) => {
setCompletedTodos(payload.todos ?? []);
});
return () => dispose();
}, []);
return (
<section>
<h2>Completed todos</h2>
{completedTodos.length === 0 ? (
<p>No completed todos received from the app.</p>
) : (
<ul>
{completedTodos.map((todo) => (
<li key={todo.id}>
<span>{todo.title}</span>
<span>{new Date(todo.completedAt).toLocaleDateString()}</span>
</li>
))}
</ul>
)}
</section>
);
}React Native:
const completedTodos = useMemo(
() => [
{ id: "1", title: "Set up RN ↔ WebView bridge", completedAt: Date.now() - 86400000 * 2 },
{ id: "2", title: "Implement download with progress", completedAt: Date.now() - 86400000 },
{ id: "3", title: "Send todos list to web", completedAt: Date.now() },
],
[]
);
const handleWebViewLoadEnd = () => {
fox.emit("app:mounted", { timestamp: Date.now() });
fox.emit("app:todos:completed", { todos: completedTodos });
};
return (
<WebView
ref={webViewRef}
onLoadEnd={handleWebViewLoadEnd}
onMessage={(e) => fox.handleMessage(e.nativeEvent.data)}
source={{ uri: "https://your-webview-url.com" }}
/>
);---
6.3 Example: native button → paint screen red
Native RN button sends an event on press; the web uses a scoped channel and once() and paints the screen red the first time.
Event: app:ui:paintRed (RN → web).
Web:
import { useEffect, useState } from "react";
import { Fox } from "fox-events";
import { createReactNativeBridge } from "fox-events/bridge-react-native";
const appScope = Fox.forScope("app");
function App() {
const [screenRed, setScreenRed] = useState(false);
useEffect(() => {
const dispose = createReactNativeBridge({
direction: "both",
filter: (name) => name.startsWith("app:"),
});
Fox.on("app:ui:paintRed", () => {
appScope.emit("ui:paintRed", {});
});
appScope.channel("ui:paintRed").once().then(() => setScreenRed(true));
return () => dispose();
}, []);
return (
<>
{screenRed && <div className="screen-red-overlay" aria-hidden />}
<div className="app">{/* rest of content */}</div>
</>
);
}Overlay CSS:
.screen-red-overlay {
position: fixed;
inset: 0;
background: #b91c1c;
z-index: 9999;
}React Native:
import { Button, View } from "react-native";
import { WebView } from "react-native-webview";
import { createFoxRNAdapter } from "fox-events/bridge-react-native";
export default function App() {
const webViewRef = useRef<WebView>(null);
const fox = useMemo(
() =>
createFoxRNAdapter({
sendToWebView: (msg) => {
const code = `window.postMessage(${JSON.stringify(msg)}, '*'); true;`;
webViewRef.current?.injectJavaScript(code);
},
filter: (name) => name.startsWith("app:"),
}),
[]
);
const handlePaintRed = () => {
fox.emit("app:ui:paintRed", {});
};
return (
<View style={{ flex: 1 }}>
<Button
title="Native React Native button sending event to WebView on tap"
onPress={handlePaintRed}
/>
<WebView
ref={webViewRef}
onMessage={(e) => fox.handleMessage(e.nativeEvent.data)}
source={{ uri: "https://your-webview-url.com" }}
/>
</View>
);
}Summary: tap button → RN emits app:ui:paintRed → bridge sends to web → listener forwards to app scope → channel("ui:paintRed").once() resolves → red overlay. Reacts only the first time because of once().