@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
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 (foron/once)TSend: Events you send to native (forsend)
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:
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Commit changes:
git commit -am 'Add new feature' - Push to branch:
git push origin feature/my-feature - 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
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Made with ❤️ for the hybrid app community
