npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-web-bridge

v0.1.3

Published

Reflection-based bridge for calling JSON-clean web libraries from React Native via a WebView.

Readme

npm version npm downloads license zero runtime deps

react-native-web-bridge

Call any browser-only JavaScript library from React Native — no native module, no per-library adapter, no hand-written glue code.

Point it at a library's bundled JS, and it discovers that library's method surface at runtime through reflection, then exposes it on the RN side as a typed, async, Proxy-backed object. The library runs in a hidden WebView (a genuine browser engine); calling it from RN feels like calling a normal async function.

const { bridge, ready, error, webView } = useLibraryBridge<ValidatorLib>({
  html: buildValidatorBridgeHtml(),
});

if (ready) {
  await bridge.isEmail("[email protected]"); // -> true
}

Zero runtime dependencies. Works in Expo Go (no dev client, no config plugin) and in a plain npx react-native init app — both confirmed live, not just assumed.

Table of contents

Why this exists

React Native has no DOM. Any web library that touches document/window/ HTMLElement directly — sanitizers, diagram renderers, barcode generators, PDF engines, OCR — has nothing to render into on the RN side. The usual answers are a native module (per-platform code, a real maintenance burden) or a hand-written WebView wrapper (manually registering every method you need, rewritten again for the next library).

This library automates the second path. It doesn't invent a new way to cross the RN↔WebView boundary — postMessage RPC is a solved, commoditized problem with half a dozen equivalent libraries. What's missing everywhere else is automatic method discovery: reflection walks the target library's prototype chain inside the WebView, builds a manifest, and a Proxy on the RN side turns every discovered method into a callable async function — without you writing a single hand-registered handler.

Install

npm install react-native-web-bridge
npx expo install react-native-webview   # or: npm install react-native-webview

Quick start

  1. Build your library's HTML bundle. The WebView needs a page that assigns your target library to window.__RNWB_TARGET__ before the bridge's injected script runs — typically a real npm package bundled with esbuild/webpack/Metro into an IIFE:

    // fixtures/validator.ts
    import { VALIDATOR_SCRIPT } from "./validatorBundle.generated"; // built by your bundler
    
    export function buildValidatorBridgeHtml() {
      return `<!DOCTYPE html><html><body><script>${VALIDATOR_SCRIPT}</script></body></html>`;
    }
    
    export interface ValidatorLib {
      isEmail(value: string): Promise<boolean>;
    }

    A library that's already a flat function bag (validator, qrcode, DOMPurify) can be assigned to window.__RNWB_TARGET__ directly. A library whose entry point is a constructor (docx, pdf-lib, pptxgenjs) needs a small wrapper object instead — build an instance, chain calls, return a plain JSON-safe result. The bridge can call methods on an existing target; it can't call a constructor directly.

  2. Use useLibraryBridge:

    import { useLibraryBridge } from "react-native-web-bridge";
    import { buildValidatorBridgeHtml, type ValidatorLib } from "./fixtures/validator";
    
    const { bridge, ready, error, webView } = useLibraryBridge<ValidatorLib>({
      html: buildValidatorBridgeHtml(),
    });
    
    // Render `webView` somewhere in the tree - it's a 0x0 hidden WebView
    // that does the actual bridging work, not a visible UI element. Pass
    // `style` in the options if the library's own rendered output (a map,
    // a chart) belongs on screen instead.

    ValidatorLib here is hand-typed (see the type safety caveat below).

What it's capable of

Every example below is a real, unmodified npm package, proven working through this exact mechanism — not a hypothetical. Each one gets its own agent.*.test.ts run against the actual library, and a live demo page in this repo's example app.

PDF & Office documents

Generate real .pdf, .docx, .xlsx, and .pptx files — and fill real Word templates — none of which have a pure-JS RN-native equivalent today.

export interface PdflibLib {
  createTextPdf(bodyText: string): Promise<{ base64: string; byteLength: number }>;
}

const { bridge } = useLibraryBridge<PdflibLib>({ html: buildPdflibBridgeHtml() });
const { base64 } = await bridge.createTextPdf("Hello from the bridge");
// base64 is a real, valid PDF - save it, share it, or hand it to another
// bridged library (see "Composing two libraries together" below).
// Fill a real Word *template* with data - docxtemplater + docx composed
export interface DocxtemplaterLib {
  createCertificate(name: string, course: string, date: string): Promise<{ base64: string }>;
}

const { base64 } = await bridge.createCertificate("Ada Lovelace", "Advanced Mathematics", "2026-07-30");

Also covered: xlsx/SheetJS (spreadsheets, read back and rendered as a real table), pptxgenjs (PowerPoint), pdfjs-dist (real PDF text extraction, Mozilla's own PDF.js engine).

Cryptography & authentication

Real JWTs via Web Crypto, real WASM password hashing, real TOTP 2FA codes — none of which RN has a built-in primitive for.

export interface JoseLib {
  signSimpleJwt(secretString: string, claims: Record<string, unknown>): Promise<string>;
  verifyJwt(token: string, secretString: string): Promise<Record<string, unknown>>;
}

const jwt = await bridge.signSimpleJwt("a-real-secret", { sub: "user123", role: "admin" });
export interface HashwasmLib {
  hashPassword(password: string, saltSeed: string): Promise<string>;   // Argon2id
  verifyPassword(password: string, hash: string): Promise<boolean>;
}

const hash = await bridge.hashPassword("correct horse battery staple", "a-real-salt");

Also covered: otpauth (RFC 6238 TOTP — the literal "set up 2FA" flow, composed with qrcode below).

HTML processing

Sanitize untrusted HTML and convert HTML to Markdown — both genuinely impossible in RN natively, since both algorithms are defined in terms of parsing and rebuilding a real DOM tree, which RN has none of.

export interface DompurifyLib {
  sanitize(dirty: string, config?: Record<string, unknown>): Promise<string>;
}

const clean = await bridge.sanitize('<img src=x onerror=alert(1)><b>Bold and safe</b>');
// -> "<b>Bold and safe</b>" - the script/handler attributes are gone, not just hidden
export interface TurndownLib {
  turndown(html: string): Promise<string>;
}

const markdown = await bridge.turndown("<h1>Release Notes</h1><p>Adds <b>real</b> Markdown export.</p>");

Diagrams & barcodes — real native SVG

The standout pattern: the library's output comes back as a plain SVG string, which renders as genuine native pixels via react-native-svg's SvgXml — zero WebView pixels in what the user actually sees, despite the WebView doing the generation.

import { SvgXml } from "react-native-svg";

export interface MermaidLib {
  renderDiagram(definition: string): Promise<string>;
}

const svg = await bridge.renderDiagram(`
  flowchart TD
    A[Start] --> B{Is it working?}
    B -->|Yes| C[Ship it]
    B -->|No| D[Debug]
    D --> B
`);

<SvgXml xml={svg} width={280} height={280} />
export interface JsbarcodeLib {
  renderBarcode(value: string, options?: Record<string, unknown>): Promise<string>;
}

const svg = await bridge.renderBarcode("012345678905", { format: "EAN13" });
<SvgXml xml={svg} width={260} height={140} />

Also covered: qrcode (the original proof of this pattern — real QR codes, plus a base64 PNG data-URL variant).

OCR — text recognition

Real WASM-based OCR (the same engine used across the browser ecosystem) — "scan a receipt/document" with no native module.

export interface TesseractLib {
  recognizeText(imageDataUrl: string): Promise<{ text: string; confidence: number }>;
}

const { text, confidence } = await bridge.recognizeText(scannedImageDataUrl);

Composing two libraries together

Because discovery is automatic for any JSON-clean-enough library, nothing stops bridging two unrelated libraries and using them together to solve a problem neither one solves alone — inside the same WebView, or across two.

// html2canvas + jsPDF: "HTML → PDF" with zero native modules, using
// capabilities only a WebView has - a real DOM to build the HTML in, and
// a real Canvas2D to rasterize it.
export interface HtmltopdfLib {
  htmlToPdf(html: string): Promise<{ base64: string; byteLength: number }>;
}

const { base64 } = await bridge.htmlToPdf(`
  <h1 style="color:#c00">Invoice #42</h1>
  <p>Total due: <b>$120.00</b></p>
`);
// otpauth + qrcode: build a real TOTP secret, then render its otpauth://
// URI as a real, scannable QR code - the literal "set up 2FA" screen.
const totp = await otpauthBridge.createTotp("Acme Inc", "[email protected]");
const qr = await qrcodeBridge.toString(totp.uri, { type: "svg" });
<SvgXml xml={qr} width={160} height={160} />

Interactive maps — visible WebView

Every example above uses a hidden 0×0 WebView — the library's job is computing a result, not rendering pixels. A library whose actual output belongs on screen (a map, in this case) can opt into a visible WebView instead, driven live by ordinary RN buttons:

export interface LeafletMapLib {
  flyToCity(lat: number, lng: number, zoom: number): Promise<{ lat: number; lng: number; zoom: number }>;
  addMarkerAtCenter(label?: string): Promise<{ lat: number; lng: number }>;
  getZoom(): Promise<number>;
}

const { bridge, webView } = useLibraryBridge<LeafletMapLib>({
  html: buildLeafletBridgeHtml(),
  style: { flex: 1 }, // <- visible, instead of the default hidden 0x0
});

<Button title="Fly to London" onPress={() => bridge.flyToCity(51.505, -0.09, 11)} />
{webView}

How it works

  1. Discovery. A small injected script (AGENT_SCRIPT) runs inside the WebView once the page loads, walks the target's prototype chain (Object.getOwnPropertyNames, own and inherited), and posts back a manifest of real method names.
  2. Proxy. The RN side turns that manifest into a JS Proxy — every discovered method becomes (...args) => Promise<result>, correlated by request id over a single ordered postMessage channel.
  3. JSON-safety boundary. Every call argument is checked before it crosses the bridge; anything that can't survive a JSON.stringify/ JSON.parse round trip (functions, undefined, circular refs, class instances) is rejected with a path-pointing error instead of silently corrupting on the wire.
  4. Handles, callbacks, events. A method returning a class instance becomes a live handle (a second Proxy routing further calls to that specific instance) instead of losing its methods. A function passed as a call argument is registered locally and revived into a real forwarding function on the WebView side — fires however many times the library actually calls it. Three event conventions (on/off, DOM's addEventListener/removeEventListener, single-callback subscribe/ unsubscribe) are auto-detected and wired the same way.

Comparison with alternatives

| | react-native-web-bridge | Hand-written WebView bridges¹ | Expo DOM Components ('use dom') | Comlink | | --- | --- | --- | --- | --- | | Automatic method discovery (no hand-registered glue per library) | Yes — reflection-based, at runtime | No — every method hand-registered | No — you hand-author the DOM-side component | No — you call Comlink.expose(obj) yourself | | Problem domain | WebView ↔ RN | WebView ↔ RN | WebView ↔ RN (Expo-only) | Worker ↔ main thread | | JSON-safety enforcement | Explicit, runtime-checked, path-pointing errors | Usually implicit (JSON.stringify does whatever it does) | Undocumented in detail | Implicit for plain values, explicit via transferHandlers for the rest | | Arbitrary functions as call arguments | Yes — any function anywhere in an argument tree | No, or hand-wired for one specific case | No — only top-level async functions | Yes — transferHandlers generalize this | | Remote object handles (stateful/chained APIs) | Yes, with a FinalizationRegistry GC safety net | No | No | Buildable on top, not built in | | Runtime dependencies | 0 | Varies | N/A (part of Expo SDK) | 0 | | Works in Expo Go (no dev client) | Yes, verified | Expected, same foundation | Yes (it's Expo's own feature) | N/A | | Works in bare react-native init CLI | Yes, verified | Expected, same foundation | No — coupled to Expo's Metro/Router | N/A | | Native rendering output | No (WebView's own pixels, unless the library's output is SVG/text) | No | No | N/A |

¹ e.g. libraries that solve postMessage RPC between RN and a WebView but require registering every method by hand — a well-trodden, commoditized pattern this library builds its transport layer on top of, then automates the part that's usually manual.

The honest trade-off: automatic discovery means method names aren't checked against the library at compile time the way a hand-written, fully-typed bridge can be — you can't statically type-check against a surface that's only known via runtime reflection. That's the direct cost of not hand-writing glue for every library.

What crosses the bridge

  • JSON-clean values — numbers, strings, plain objects/arrays — cross directly, both directions.
  • Functions as call arguments are supported generally, not just for a hardcoded on/off pair — a function anywhere in a call's arguments gets registered locally and revived into a real forwarding function on the WebView side. Fires however many times the library actually calls it.
  • Event subscriptions auto-detect three conventions: on/off, DOM's addEventListener/removeEventListener, and a single-callback subscribe/unsubscribe (no event name).
  • Class-instance / chained-API results (map.getCenter(), a fluent builder) come back as a live handle — a Proxy whose methods route to that specific instance — instead of losing their methods. Call handle.release() when done; there's also a FinalizationRegistry safety net, but treat it as a backstop, not a substitute.
  • Binary results (a generated PDF/zip/image) need the target's own method to base64-encode them before returning — a raw Uint8Array is a class instance, not JSON-safe, same as any other one.

Known caveats

  • The JSON-safety boundary is real, not just documented. Anything that can't survive a JSON.stringify/JSON.parse round trip (functions, undefined, class instances, circular refs) either gets rejected before crossing the bridge (call arguments) or degrades with a warning attached to the response (call results/event payloads).
  • Remote object handles need explicit release(). The FinalizationRegistry-based automatic cleanup is a safety net only — GC timing is non-deterministic.
  • Type safety is generated, not enforced. The generic type parameter passed to useLibraryBridge<T> isn't checked against the library at compile time (it can't be — the library's shape is only known via runtime reflection). A companion codegen script can generate method-name stubs from a real discovery run instead of a hand-typed guess, closing the typo/drift gap — it can't infer parameter/return types, only names.
  • Synchronous call semantics don't cross the bridge. Every bridged call is async, regardless of what the original library's own API looks like — a different JS engine/process (the WebView) is a hard boundary, not a transport limitation.
  • No native rendering. Output is always the WebView's own pixels, wired up as a live handle you can call methods on, not something that reconciles into native View/Text components. Libraries that produce SVG/text output (barcode/QR/diagram generators, PDF text extraction) can still end up as genuine native pixels — render the returned markup with react-native-svg or plain RN components — since in that case the WebView is only ever doing the computation, not the on-screen rendering.
  • A library needing live network access at runtime (OCR's trained-data download on first use, for example) isn't fully self-contained after bundling — worth knowing before relying on one offline.

License

MIT