Fox Events Fox Events

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

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

ts
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

ts
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):

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:

ts
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):

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:

ts
const fox = useMemo(() => createFoxRNAdapter({ sendToWebView: (msg) => { /* ... */ } }), []);

useEffect(() => {
  setFox(fox);
}, [fox]);

Em qualquer tela: use getFox():

ts
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):

ts
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)

ts
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 chama Fox.emit(name, payload); qualquer Fox.on("app:command", ...) na web é executado.
  • Web → RN: Fox.emit("app:ready", payload) na web; o bridge envia ao RN via ReactNativeWebView.postMessage; no RN, onMessage chama fox.handleMessage(data); qualquer fox.on("app:ready", ...) no RN é executado.

Exemplo — Web:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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:

ts
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:

css
.screen-red-overlay {
  position: fixed;
  inset: 0;
  background: #b91c1c;
  z-index: 9999;
}

React Native:

ts
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 appchannel("ui:paintRed").once() resolve → overlay vermelho. Reage só na primeira vez por causa de once().