Bridge React Native
Mensagens bidirecionais entre React Native e WebView. Os trechos de código desta página usam syntax highlighting com tema Dracula (Prism) na documentação gerada.
---
Índice
- 1. Introdução
- 2. Configuração
- 2.1 Lado WebView
- 2.2 Lado RN - Adapter
- 3. Boas práticas
- 3.1 WebView: bridge global
- 3.2 RN: uso global do adapter
- 4. Uso (API)
- 5. Fluxo RN - Web
- 6. Exemplos de implementação
- 6.1 Download com progresso
- 6.2 Lista de todos concluídos
- 6.3 Botão nativo - pintar tela de vermelho
---
1. Introdução
O Bridge permite usar a mesma API do Fox Events no app React Native (via adapter) e dentro da WebView (via configuração mínima). Eventos emitidos em um ambiente são recebidos no outro — comandos, dados ou eventos de ciclo de vida (ex.: “web pronta”, “recarregar”) sem cola customizada de postMessage.
Configure o bridge uma vez no lado WebView com createReactNativeBridge e no lado RN com createFoxRNAdapter e o onMessage / inject da WebView. Depois use fox.emit, fox.on e fox.once no RN e Fox.emit / Fox.on na aplicação web.
---
2. Configuração
2.1 Lado WebView
import { createReactNativeBridge } from "fox-events/bridge-react-native";
const dispose = createReactNativeBridge({
direction: "both",
filter: (name) => name.startsWith("app:"),
debug: false,
});
dispose();2.2 Lado RN — 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. Boas práticas
3.1 WebView: bridge global
Inicialize o bridge uma vez, em um módulo importado no entry point. Qualquer componente pode usar Fox.emit e Fox.on sem repetir a configuração.
Módulo do bridge (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): importe antes do App:
import "./bridge";
import App from "./App";3.2 RN: uso global do adapter
Para usar o mesmo adapter em várias telas sem passar props, registre-o globalmente.
Opção A — Módulo global (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;
}No layout com WebView: chame setFox(fox) após criar o adapter:
const fox = useMemo(() => createFoxRNAdapter({ sendToWebView: (msg) => { /* ... */ } }), []);
useEffect(() => {
setFox(fox);
}, [fox]);Em qualquer tela: use getFox():
import { getFox } from "@/app/fox-global";
export default function OutraTela() {
const fox = getFox();
const handleEnviar = () => fox.emit("app:comando", { id: "1" });
useEffect(() => {
const unsub = fox.on("app:resposta", (p) => console.log(p));
return () => unsub();
}, [fox]);
return (/* ... */);
}Opção B — Hook que usa o 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();
}Uso: const fox = useFox();. Para injetar via árvore React, use createContext e <FoxContext.Provider value={fox}> no layout.
---
4. Uso (API)
fox.emit("app:command", { action: "reload" });
fox.on("app:ready", (payload) => console.log(payload));
const payload = await fox.once("app:login");---
5. Fluxo RN ↔ Web
- RN → Web:
fox.emit("app:command", payload)no RN; o bridge na web chamaFox.emit(name, payload); qualquerFox.on("app:command", ...)na web é executado. - Web → RN:
Fox.emit("app:ready", payload)na web; o bridge envia ao RN viaReactNativeWebView.postMessage; no RN,onMessagechamafox.handleMessage(data); qualquerfox.on("app:ready", ...)no RN é executado.
Exemplo — 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" });Exemplo — React Native:
fox.emit("app:command", { action: "reload" });
fox.on("app:ready", (payload) => {
console.log("Web disse pronto:", payload.version);
});---
6. Exemplos de implementação
6.1 Exemplo: download com progresso
Botão na WebView dispara “download”; o RN simula e envia progresso; a web mostra barra e toast ao concluir.
Eventos: 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 concluído!");
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 ? `Baixando... ${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://sua-url-da-webview.com" }}
/>
);
}Resumo do fluxo: 1) Web emite app:download:start. 2) RN simula e emite app:download:progress. 3) Web atualiza barra. 4) RN emite app:download:complete. 5) Web mostra toast.
---
6.2 Exemplo: lista de todos concluídos
RN envia lista de todos concluídos no onLoadEnd; a web recebe e renderiza.
Evento: 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>Todos concluídos</h2>
{completedTodos.length === 0 ? (
<p>Nenhum todo concluído recebido do app.</p>
) : (
<ul>
{completedTodos.map((todo) => (
<li key={todo.id}>
<span>{todo.title}</span>
<span>{new Date(todo.completedAt).toLocaleDateString("pt-BR")}</span>
</li>
))}
</ul>
)}
</section>
);
}React Native:
const completedTodos = useMemo(
() => [
{ id: "1", title: "Configurar bridge RN ↔ WebView", completedAt: Date.now() - 86400000 * 2 },
{ id: "2", title: "Implementar download com progresso", completedAt: Date.now() - 86400000 },
{ id: "3", title: "Enviar lista de todos para a 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://sua-url-da-webview.com" }}
/>
);---
6.3 Exemplo: botão nativo → pintar tela de vermelho
Botão nativo RN envia evento ao toque; a web usa scoped channel e once() e pinta a tela de vermelho na primeira vez.
Evento: 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">{/* resto do conteúdo */}</div>
</>
);
}CSS do overlay:
.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="Botão nativo React Native enviando evento para WebView após o tap"
onPress={handlePaintRed}
/>
<WebView
ref={webViewRef}
onMessage={(e) => fox.handleMessage(e.nativeEvent.data)}
source={{ uri: "https://sua-url-da-webview.com" }}
/>
</View>
);
}Resumo: toque no botão → RN emite app:ui:paintRed → bridge envia para a web → listener repassa para o escopo app → channel("ui:paintRed").once() resolve → overlay vermelho. Reage só na primeira vez por causa de once().