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

@ugursahinkaya/native-bridge

v0.1.0

Published

> Universal, type-safe, framework-agnostic bridge for hybrid/native apps

Readme

@ugursahinkaya/native-bridge

Universal, type-safe, framework-agnostic bridge for hybrid/native apps

npm version License: MIT TypeScript

Zero-configuration bridge that automatically detects and integrates with 10+ native platforms. Write once, run everywhere.


✨ Features

  • 🎯 Auto-detection: Automatically detects Flutter, React Native, Electron, Tauri, Cordova, and 6 more platforms
  • 🔒 Type-safe: Full TypeScript support with generic event maps
  • 🔄 Bidirectional: Send events to native, receive events from native
  • 🎭 Framework-agnostic: Works with any JavaScript framework or vanilla JS
  • 🪶 Lightweight: ~4KB minified
  • 🧩 Zero dependencies: No external dependencies
  • 🛡️ SSR-safe: Built-in guards for server-side rendering
  • 🎪 Singleton pattern: Single global message listener (no memory leaks)

📦 Installation

npm install @ugursahinkaya/native-bridge
# or
pnpm add @ugursahinkaya/native-bridge
# or
yarn add @ugursahinkaya/native-bridge

🚀 Quick Start

import { useNativeBridge } from '@ugursahinkaya/native-bridge';

// Define your event contracts
type NativeEvents = {
  'user-login': { token: string; userId: string };
  'app-ready': {};
};

type JsEvents = {
  'request-login': { username: string };
  'logout': {};
};

const bridge = useNativeBridge<NativeEvents, JsEvents>();

// ✅ Type-safe: Listen to events from native
bridge.on('user-login', ({ token, userId }) => {
  console.log('User logged in:', userId);
});

// ✅ Type-safe: Send events to native
bridge.send('request-login', { username: 'alice' });

// ✅ Listen once and auto-unsubscribe
bridge.once('app-ready', () => {
  console.log('App is ready!');
});

// ✅ Manual cleanup
const unsubscribe = bridge.on('user-login', handler);
unsubscribe(); // Remove listener

🌍 Supported Platforms

The bridge automatically detects the runtime environment:

| Platform | Detection | Status | |----------|-----------|--------| | Flutter (inappwebview) | window.flutter_inappwebview | ✅ | | Flutter (webview_flutter) | window.JSBridge | ✅ | | React Native | window.ReactNativeWebView | ✅ | | Android WebView | window.AndroidInterface | ✅ | | iOS WKWebView | window.webkit.messageHandlers | ✅ | | Capacitor | window.Capacitor | ✅ | | Ionic/Cordova | window.cordova | ✅ | | Electron | window.electronAPI | ✅ | | Tauri | window.__TAURI__ | ✅ | | NW.js | window.nw | ✅ | | Generic postMessage | window.parent | ✅ |

If no platform is detected, the bridge operates in safe mode (logs info, no errors).


📖 API Reference

useNativeBridge<TReceive, TSend>()

Returns a bridge instance with type-safe methods.

Type Parameters

  • TReceive: Events you receive from native (for on/once)
  • TSend: Events you send to native (for send)

Methods

on(eventName, handler): () => void

Listen to events from native. Returns an unsubscribe function.

const unsubscribe = bridge.on('user-login', ({ token }) => {
  console.log('Token:', token);
});

// Later: cleanup
unsubscribe();
once(eventName, handler): () => void

Listen to an event only once, then auto-unsubscribe.

bridge.once('app-ready', () => {
  console.log('App initialized!');
});
off(eventName, handler): void

Manually remove an event listener.

const handler = (payload) => console.log(payload);
bridge.on('user-login', handler);

// Later: remove
bridge.off('user-login', handler);
send(eventName, payload): void

Send an event to native.

bridge.send('request-login', { username: 'alice' });
bridge.send('logout', {});
bridgeType: string

Get the detected platform type.

console.log(bridge.bridgeType); // 'flutter_inappwebview', 'react_native', etc.

🔧 Advanced Usage

React Integration

import { useEffect } from 'react';
import { useNativeBridge } from '@ugursahinkaya/native-bridge';

function App() {
  const bridge = useNativeBridge<NativeEvents, JsEvents>();

  useEffect(() => {
    const unsubscribe = bridge.on('user-login', ({ token }) => {
      console.log('User logged in:', token);
    });

    return () => unsubscribe(); // Cleanup on unmount
  }, []);

  const handleLogin = () => {
    bridge.send('request-login', { username: 'alice' });
  };

  return <button onClick={handleLogin}>Login</button>;
}

Vue Integration

import { onMounted, onUnmounted } from 'vue';
import { useNativeBridge } from '@ugursahinkaya/native-bridge';

export default {
  setup() {
    const bridge = useNativeBridge<NativeEvents, JsEvents>();
    let unsubscribe: (() => void) | null = null;

    onMounted(() => {
      unsubscribe = bridge.on('user-login', ({ token }) => {
        console.log('User logged in:', token);
      });
    });

    onUnmounted(() => {
      unsubscribe?.();
    });

    const handleLogin = () => {
      bridge.send('request-login', { username: 'alice' });
    };

    return { handleLogin };
  }
};

Svelte Integration

<script lang="ts">
  import { onMount } from 'svelte';
  import { useNativeBridge } from '@ugursahinkaya/native-bridge';

  const bridge = useNativeBridge<NativeEvents, JsEvents>();

  onMount(() => {
    const unsubscribe = bridge.on('user-login', ({ token }) => {
      console.log('User logged in:', token);
    });

    return () => unsubscribe();
  });

  function handleLogin() {
    bridge.send('request-login', { username: 'alice' });
  }
</script>

<button on:click={handleLogin}>Login</button>

🛠️ Native Integration Examples

Flutter (inappwebview)

// Dart side - Receive from JS
webViewController.addJavaScriptHandler(
  handlerName: 'request-login',
  callback: (args) {
    print('Username: ${args[0]['username']}');
    return {'token': 'abc123', 'userId': '42'};
  }
);

// Dart side - Send to JS
webViewController.evaluateJavascript(source: '''
  window.postMessage({ type: 'user-login', token: 'abc123', userId: '42' }, '*');
''');

Flutter (webview_flutter)

// Dart side - Receive from JS
JavascriptChannel(
  name: 'JSBridge',
  onMessageReceived: (msg) {
    final data = jsonDecode(msg.message);
    if (data['type'] == 'request-login') {
      print('Username: ${data['username']}');
    }
  },
)

// Dart side - Send to JS
webViewController.runJavascript('''
  window.postMessage({ type: 'user-login', token: 'abc123', userId: '42' }, '*');
''');

React Native

// React Native side
import { WebView } from 'react-native-webview';

<WebView
  onMessage={(event) => {
    const { type, ...payload } = JSON.parse(event.nativeEvent.data);
    if (type === 'request-login') {
      // Handle login
      webViewRef.current.postMessage(JSON.stringify({
        type: 'user-login',
        token: 'abc123',
        userId: '42'
      }));
    }
  }}
/>

Electron

// Main process (preload.js)
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
  send: (channel, data) => ipcRenderer.send(channel, data)
});

// Main process
ipcMain.on('native-bridge', (event, { type, ...payload }) => {
  if (type === 'request-login') {
    event.sender.send('window.postMessage', {
      type: 'user-login',
      token: 'abc123'
    });
  }
});

Tauri

// Rust side
#[tauri::command]
async fn native_bridge_handler(r#type: String, payload: serde_json::Value) -> Result<(), String> {
    if r#type == "request-login" {
        // Handle login
    }
    Ok(())
}

// Send to JS (in Tauri v1)
app_handle.emit_all("user-login", json!({ "token": "abc123", "userId": "42" })).unwrap();

⚡ Best Practices

1. Always Cleanup Listeners

// ❌ Memory leak
bridge.on('user-login', handler);

// ✅ Proper cleanup
const unsubscribe = bridge.on('user-login', handler);
// Later or in cleanup hook:
unsubscribe();

2. Use once() for One-Time Events

// ❌ Manual cleanup
const unsubscribe = bridge.on('app-ready', () => {
  console.log('Ready!');
  unsubscribe();
});

// ✅ Auto-cleanup with once
bridge.once('app-ready', () => {
  console.log('Ready!');
});

3. Define Type-Safe Event Contracts

// ✅ Centralized types
// types/native-events.ts
export type NativeEvents = {
  'user-login': { token: string; userId: string };
  'user-logout': {};
  'app-ready': {};
};

export type JsEvents = {
  'request-login': { username: string; password: string };
  'request-logout': {};
};

// app.ts
import type { NativeEvents, JsEvents } from './types/native-events';
const bridge = useNativeBridge<NativeEvents, JsEvents>();

4. Handle Errors Gracefully

try {
  bridge.send('critical-action', { data: 'important' });
} catch (error) {
  console.error('Bridge communication failed:', error);
  // Fallback logic
}

5. Check Bridge Type for Platform-Specific Logic

const bridge = useNativeBridge();

if (bridge.bridgeType === 'flutter_inappwebview') {
  // Flutter-specific logic
} else if (bridge.bridgeType === 'react_native') {
  // React Native-specific logic
}

🐛 Troubleshooting

Events Not Received

Problem: Native events not triggering listeners

Solutions:

// 1. Check if bridge is initialized
console.log('Bridge type:', bridge.bridgeType);

// 2. Verify event name matches exactly
bridge.on('user-login', handler); // Must match native side

// 3. Check message format from native
// Native must send: { type: 'user-login', ...payload }

SSR/Server-Side Rendering Issues

Problem: window is not defined error

Solution: Bridge has built-in SSR guards, but wrap in client-side check:

// Next.js example
import dynamic from 'next/dynamic';

const BridgeComponent = dynamic(() => import('./BridgeComponent'), {
  ssr: false
});

TypeScript Errors

Problem: Type errors with event names/payloads

Solution:

// ✅ Use string literal types
type Events = {
  'user-login': { token: string }; // Not just 'string'
};

// ✅ Use 'as const' for event names
const EVENT_NAMES = {
  USER_LOGIN: 'user-login'
} as const;

Memory Leaks

Problem: Listeners not cleaned up

Solution:

// React
useEffect(() => {
  const unsubscribe = bridge.on('event', handler);
  return () => unsubscribe(); // Cleanup
}, []);

// Vue
onUnmounted(() => unsubscribe());

// Svelte
onDestroy(() => unsubscribe());

📊 Performance

  • Bundle size: ~4KB minified
  • Runtime overhead: Negligible (singleton pattern, single global listener)
  • Memory: O(n) where n = number of active listeners
  • No polling: Event-driven architecture

🔒 Security Considerations

  • postMessage origin: Currently accepts all origins (*) for maximum compatibility
  • For production: Consider adding origin validation:
// Custom wrapper with origin validation
function createSecureBridge(allowedOrigins: string[]) {
  const bridge = useNativeBridge();
  
  window.addEventListener('message', (event) => {
    if (!allowedOrigins.includes(event.origin)) {
      console.warn('Rejected message from:', event.origin);
      return;
    }
  });
  
  return bridge;
}

🤝 Contributing

Contributions are welcome! Please follow these steps:

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/my-feature
  3. Commit changes: git commit -am 'Add new feature'
  4. Push to branch: git push origin feature/my-feature
  5. Submit a pull request

📄 License

MIT © Ugur Sahin Kaya


🙏 Acknowledgements

Inspired by the need for a universal, type-safe bridge across all hybrid/native platforms.


📮 Support


Made with ❤️ for the hybrid app community