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

@gaurav-tewari/expo-webview-bridge

v1.0.0

Published

Bidirectional message bridge between React Native and WebView for Expo

Readme

@gaurav-tewari/expo-webview-bridge

A lightweight, fully-typed bidirectional message bridge between React Native and WebView for Expo. Send typed messages in both directions, await replies with promises, pass initial config, set cookies and storage — all with a clean React API.

Preview

Features

  • Bidirectional messaging — RN → WebView and WebView → RN with typed payloads
  • Request/responsesendRequest returns a Promise resolved by a web-side reply() call
  • Message queuing — messages sent before the bridge is ready are queued and flushed automatically on onReady
  • Auto-injected bridgewindow.Bridge is available in the WebView before the page scripts run
  • Initial params — pass read-only config/tokens into the WebView as Bridge.params
  • Web storage — pre-populate cookies, localStorage, and sessionStorage
  • onClose — let the web page signal RN to unmount the WebView
  • onBridgeError — unified error callback covering both directions of the bridge
  • Wildcard subscriptionsBridge.on('*', handler) catches all message types
  • once — subscribe for a single message on both the RN and web sides
  • Full TypeScript — everything is typed end-to-end
  • ESM + CJS — ships both module formats for Metro and web bundlers

Installation

npx expo install react-native-webview
npm install @gaurav-tewari/expo-webview-bridge

Expo Goreact-native-webview requires native code and is not bundled in the standard Expo Go client. Use a development build (npx expo run:ios / npx expo run:android) to test.

Quick Start

import React, { useRef } from 'react';
import { View, Button } from 'react-native';
import { WebViewBridge, WebViewBridgeRef } from '@gaurav-tewari/expo-webview-bridge';

const HTML = `
  <button onclick="Bridge.send('hello', { from: 'web' })">Send to RN</button>
  <script>
    Bridge.on('ping', (payload) => console.log('RN says:', payload));
  </script>
`;

export default function App() {
  const ref = useRef<WebViewBridgeRef>(null);

  return (
    <View style={{ flex: 1 }}>
      <Button title="Ping WebView" onPress={() => ref.current?.sendMessage('ping', { ts: Date.now() })} />
      <WebViewBridge
        ref={ref}
        source={{ html: HTML }}
        onMessage={(type, payload) => console.log('[RN]', type, payload)}
        onReady={() => console.log('bridge ready')}
      />
    </View>
  );
}

Architecture

React Native                              WebView
──────────────────────────────────────────────────────────────
ref.sendMessage('type', payload)  ──►  Bridge.on('type', handler)
ref.sendRequest('type', payload)  ──►  Bridge.on('type', (p, t, reply) => reply(result))
  └─ Promise<result>              ◄──  reply(result)
onMessage(type, payload)          ◄──  Bridge.send('type', payload)
onClose()                         ◄──  Bridge.close()
onBridgeError(error)              ◄──  Bridge.on() handler throws
                                        (reported as __bridge_error__)

The bridge script is injected via injectedJavaScriptBeforeContentLoaded, so window.Bridge (and Bridge.params) are available synchronously before any page script runs. Messages sent before onReady fires are queued and delivered automatically once the bridge initialises.


API Reference

<WebViewBridge>

Extends all react-native-webview WebView props. The following props are added or replaced:

| Prop | Type | Description | |---|---|---| | onMessage | (type: string, payload: unknown) => void | Called for every user message received from the WebView. | | onReady | () => void | Called once the bridge script has initialised inside the WebView. | | onClose | () => void | Called when the web side invokes Bridge.close(). Use this to unmount or hide the WebView. | | onBridgeError | (error: BridgeError) => void | Called when the bridge encounters an error in either direction. | | initialParams | Record<string, unknown> | Data passed into the WebView as Bridge.params (read-only, available before the page loads). | | webStorage | WebStorageConfig | Cookies, localStorage, and sessionStorage entries written after the page loads. | | injectedJavaScriptBeforeContentLoaded | string | Extra JS injected alongside the bridge script (before page load). |

onMessage and injectedJavaScriptBeforeContentLoaded from react-native-webview are replaced by the bridge's versions — use the props above instead.


Ref — WebViewBridgeRef

const ref = useRef<WebViewBridgeRef>(null);
<WebViewBridge ref={ref} ... />

| Method | Description | |---|---| | sendMessage(type, payload?) | Send a typed message into the WebView. Queued automatically if the bridge isn't ready yet. | | sendRequest(type, payload?, options?) | Send a message and return a Promise resolved when the web-side handler calls reply(data). Rejects after options.timeout ms (default 10000). | | on(type, handler) | Subscribe to a message type from the WebView. Returns an unsubscribe function. | | once(type, handler) | Like on, but automatically unsubscribes after the first matching message. | | off(type, handler) | Remove a specific handler. |


Web-side Bridge API

Available as window.Bridge inside the WebView (no import needed):

| Member | Description | |---|---| | Bridge.params | Read-only object containing initialParams from RN. Available synchronously. | | Bridge.send(type, payload?) | Send a message to React Native. | | Bridge.close() | Signal React Native to close/unmount the WebView (triggers onClose). | | Bridge.on(type, handler) | Subscribe to a message from RN. Handler receives (payload, type, reply?). Returns an unsubscribe function. | | Bridge.once(type, handler) | Like on, but automatically unsubscribes after the first matching message. | | Bridge.off(type, handler) | Remove a specific handler. | | Bridge.on('*', handler) | Wildcard — receives every message type. |

When RN calls sendRequest, the handler's third argument reply is a function — call reply(responsePayload) to resolve the promise on the RN side.


Examples

Passing initial config

<WebViewBridge
  source={{ uri: 'https://myapp.com' }}
  initialParams={{ userId: '42', theme: 'dark', token: 'Bearer abc' }}
/>
// Inside the WebView — available synchronously before page scripts run
const { userId, theme, token } = Bridge.params;

Request / response

Ask the WebView to do something and await its answer:

// React Native side
const result = await ref.current.sendRequest<{ id: number }, { name: string }>(
  'getUser',
  { id: 42 },
  { timeout: 5000 },
);
console.log(result.name);
// Web side — reply() resolves the Promise on the RN side
Bridge.on('getUser', async (payload, type, reply) => {
  const user = await fetchUser(payload.id);
  reply({ name: user.name });
});

One-shot subscriptions

// RN — fire once, then clean up automatically
ref.current.once('authToken', (token) => {
  storeToken(token);
});
// Web — same API
Bridge.once('config', (cfg) => {
  applyTheme(cfg.theme);
});

Message queuing

Messages sent before onReady fires are queued automatically — no need to wait:

// Safe to call immediately on mount, even before the bridge is ready
useEffect(() => {
  ref.current?.sendMessage('init', { userId });
}, []);

<WebViewBridge ref={ref} source={{ uri: 'https://myapp.com' }} />

Closing the WebView

const [visible, setVisible] = useState(true);

{visible && (
  <WebViewBridge
    source={{ uri: 'https://myapp.com' }}
    onClose={() => setVisible(false)}
  />
)}
document.getElementById('close-btn').onclick = () => Bridge.close();

Pre-populating storage

<WebViewBridge
  source={{ uri: 'https://myapp.com' }}
  webStorage={{
    cookies: [
      { name: 'session', value: 'abc-xyz', path: '/', maxAge: 3600 },
    ],
    localStorage: { authToken: 'Bearer eyJhbGc...' },
    sessionStorage: { lastRoute: '/dashboard' },
  }}
/>

Error Handling

<WebViewBridge
  onBridgeError={(error) => {
    console.error(error.source, error.message, error.detail);
  }}
/>
interface BridgeError {
  source: 'rn-to-webview' | 'webview-to-rn' | 'webview-internal';
  message: string;
  detail?: unknown;
}

License

MIT