https://github.com/l2hyunwoo/nitro-webview
Nitro + WebView = π
https://github.com/l2hyunwoo/nitro-webview
nitro-modules react-native webview
Last synced: 8 days ago
JSON representation
Nitro + WebView = π
- Host: GitHub
- URL: https://github.com/l2hyunwoo/nitro-webview
- Owner: l2hyunwoo
- License: mit
- Created: 2026-05-31T02:57:33.000Z (29 days ago)
- Default Branch: main
- Last Pushed: 2026-06-01T16:09:37.000Z (27 days ago)
- Last Synced: 2026-06-09T05:33:10.446Z (20 days ago)
- Topics: nitro-modules, react-native, webview
- Language: TypeScript
- Homepage:
- Size: 1.85 MB
- Stars: 23
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# nitro-webview
iOS
Android
A React Native WebView built on [Nitro Modules][nitro] β pure Swift / Kotlin native sides, JSI-direct prop and event dispatch, no bridge round-trips.
## Introduction
`nitro-webview` is a drop-in WebView component for React Native that replaces the legacy bridge with [Nitro Modules][nitro]'s JSI-direct dispatch. It targets two audiences:
- **Experienced RN + Nitro developers** who want a WebView that participates in the Nitro view contract β `getHostComponent`, hybrid refs, `callback(...)` event handlers, `Promise` method results β without paying for JSON serialization or thread-hops on every prop update or event.
- **Teams evaluating WebView libraries** ("comparison shoppers") who already use `react-native-webview` and want to know what they keep, what changes, and what improves before they switch.
### What you keep coming from `react-native-webview`
- Same conceptual props (`source`, `userAgent`, `injectedJavaScript`, `onLoadStart` / `onLoadEnd`, `onMessage`, `onError`, `onShouldStartLoadWithRequest`, `onFileDownload`).
- Same `window.ReactNativeWebView.postMessage(...)` page-side contract.
- Same `originWhitelist`-style default (`['http://*', 'https://*']`) exposed as `DEFAULT_ORIGIN_WHITELIST`.
- Same `WebViewNavigationType` string union (`'click' | 'formsubmit' | 'backforward' | 'reload' | 'formresubmit' | 'other'`) so existing call-sites compile unchanged.
### What changes
- Event props must be wrapped in `callback(...)` from `react-native-nitro-modules` so Nitro can dispatch them on the right thread.
- `onShouldStartLoadWithRequest` returns `Promise` directly β no `lockIdentifier` round-trip. `async` callbacks are awaited transparently.
- Imperative methods (`goBack`, `evaluateJavaScript`, `getCookies`, `setCookie`, `clearCookies`, β¦) live on the **hybrid ref** captured via the `hybridRef` prop, not on a React `ref`.
- Native packages: `io.github.l2hyunwoo.nitrowebview` (Android) / `NitroWebView` Swift module (iOS). MIT-licensed, npm-published as `nitro-webview` (unscoped).
### Why Nitro
Nitro Modules pipes props, methods, and event callbacks through JSI so a load event or a cookie read does not round-trip through `NativeEventEmitter` or the bridge's serialization queue. For a WebView β which is event-heavy (navigation, messages, errors, downloads) β that is the main practical win.
## Quick Start
### 1. Install
```sh
yarn add nitro-webview react-native-nitro-modules
cd ios && pod install
```
`react-native-nitro-modules` is a peer dependency β install it explicitly so your dependency graph stays deterministic.
### 2. Render a WebView
```tsx
import { NitroWebView, callback } from 'nitro-webview'
export default function Screen() {
return (
console.log('loaded'))}
/>
)
}
```
Every event prop must be wrapped in `callback(...)` so Nitro can dispatch it on the right thread. Passing a raw function will throw at render time.
### 3. Call imperative methods
```tsx
import { useRef } from 'react'
import { NitroWebView, callback, type NitroWebViewType } from 'nitro-webview'
export default function Screen() {
const ref = useRef(null)
return (
<>
{
ref.current = r
})}
/>
ref.current?.reload()} />
>
)
}
```
### 4. Configure platform setup
iOS and Android both need a small amount of host-app configuration for file upload and download to work β see [Platform setup](#platform-setup) below.
## API Reference
### `NitroWebView` component
The exported React component. Backed by `getHostComponent('NitroWebView', () => NitroWebViewConfig)`.
#### Props
| Prop | Type | Notes |
| --- | --- | --- |
| `source` | `WebViewSource` | `{ uri, headers? }` or `{ html, baseUrl? }`. Drives navigation. Required. |
| `defaultHeaders` | `Record` | Global HTTP headers attached to every main-frame navigation request. Per-request `source.headers` win on key conflict. |
| `userAgent` | `string` | Overrides the platform default UA for every request (main-frame + sub-resource). `undefined` / empty restores the WebKit / Chromium default. |
| `injectedJavaScript` | `string` | Fire-and-forget script run on every page load. |
| `onLoadStart` | `(event: WebViewLoadEvent) => void` | Fired when the WebView begins loading content. |
| `onLoadEnd` | `(event: WebViewLoadEvent) => void` | Fired when the WebView finishes loading content. |
| `onNavigationStateChange` | `(state: WebViewNavigationState) => void` | URL / title / `canGoBack` / `canGoForward` / `loading`. |
| `onMessage` | `(event: WebViewMessageEvent) => void` | Fires when the page calls `window.ReactNativeWebView.postMessage(...)`. |
| `onError` | `(event: NitroWebViewErrorEvent) => void` | Navigation failure (network, SSL). |
| `onFileDownload` | `(event: FileDownloadEvent) => void` | Native intercepts a download and surfaces `{ url, mimeType?, fileName?, contentLength?, userAgent? }`. Storage is the JS layer's responsibility. |
| `onShouldStartLoadWithRequest` | `(event: ShouldStartLoadRequest) => boolean \| Promise` | Allow/block each navigation before it starts. Returning `false` (or a `Promise` resolving to `false`) cancels silently. |
#### Methods (via `hybridRef`)
The hybrid ref captured by `hybridRef={callback((r) => ref.current = r)}` exposes:
| Method | Return | Notes |
| --- | --- | --- |
| `goBack()` | `void` | Navigate back in history. |
| `goForward()` | `void` | Navigate forward in history. |
| `reload()` | `void` | Reload the current page. |
| `stopLoading()` | `void` | Stop the current load. |
| `evaluateJavaScript(code)` | `Promise` | Result is the serialized string evaluation. iOS uses `String(describing:)`; Android uses the JSON-encoded `ValueCallback` result. Undefined/nil surfaces as `''`. |
| `getCookies(url)` | `Promise` | iOS returns the full attribute set. Android `CookieManager` only exposes `name` and `value` on read β other fields are left `undefined`. |
| `setCookie(url, cookie)` | `Promise` | `Cookie = { name, value, domain?, path?, expires?, secure?, httpOnly? }`. `expires` is milliseconds since epoch (`Date.now()`-compatible). |
| `clearCookies()` | `Promise` | Bulk clear via `WKWebsiteDataStore` (iOS) / `CookieManager.removeAllCookies` (Android). The promise resolves only after the platform reports completion. |
### Types
#### `WebViewSource`
```ts
type WebViewSource = UriSource | HtmlSource
interface UriSource {
uri: string
headers?: Record
}
interface HtmlSource {
html: string
baseUrl?: string
}
```
`UriSource.headers` are per-request HTTP headers attached only to the main-frame navigation a `source` change triggers. Redirects, sub-frames, and sub-resource requests do not re-apply them.
#### `ShouldStartLoadRequest`
```ts
interface ShouldStartLoadRequest {
url: string
navigationType: WebViewNavigationType
mainDocumentURL?: string // iOS only
isTopFrame?: boolean // iOS only
hasTargetFrame?: boolean // iOS only β false for target=_blank
}
type WebViewNavigationType =
| 'click' | 'formsubmit' | 'backforward'
| 'reload' | 'formresubmit' | 'other'
```
Android leaves the three optional fields `undefined` because `WebViewClient.shouldOverrideUrlLoading` does not expose them, and always reports `navigationType: 'other'`.
The JS callback may be `async` β the bridge transparently awaits any returned thenable before applying the decision.
#### `WebViewNavigationState` & `WebViewLoadEvent`
```ts
interface WebViewNavigationState {
url: string
title: string
loading: boolean
canGoBack: boolean
canGoForward: boolean
}
interface WebViewLoadEvent {
nativeEvent: WebViewNavigationState
}
```
#### `WebViewMessageEvent`
```ts
interface WebViewMessageNativeEvent {
data: string // literal string from window.ReactNativeWebView.postMessage(...)
url: string
}
interface WebViewMessageEvent {
nativeEvent: WebViewMessageNativeEvent
}
```
#### `NitroWebViewErrorEvent`
```ts
interface NitroWebViewErrorNativeEvent {
code: number // NSError.code (iOS) / WebResourceError.getErrorCode() (Android)
description: string // localizedDescription (iOS) / getDescription().toString() (Android)
url: string // empty string when neither delegate nor error provided one
domain: string // NSError.domain (iOS) / stable string mirror (Android)
}
interface NitroWebViewErrorEvent {
nativeEvent: NitroWebViewErrorNativeEvent
}
type WebViewErrorEvent = NitroWebViewErrorEvent // alias
```
#### `Cookie`
```ts
interface Cookie {
name: string
value: string
domain?: string // platform-derived from url when omitted
path?: string // defaults to '/'
expires?: number // ms since Unix epoch; omit for a session cookie
secure?: boolean // restrict to HTTPS
httpOnly?: boolean // hide from document.cookie
}
```
#### `FileDownload` & `FileDownloadEvent`
```ts
interface FileDownload {
url: string // always http/https β blob: URLs are out of scope
mimeType?: string
fileName?: string // iOS: URLResponse.suggestedFilename
// Android: DownloadUtils.guessFileName (Content-Disposition)
contentLength?: number // -1 or absent when the platform did not supply a length
userAgent?: string // typically absent on iOS
}
interface FileDownloadEvent {
nativeEvent: FileDownload
}
```
### Origin whitelist helpers
Pure-TS helpers for building allowlist-style policies on top of `onShouldStartLoadWithRequest`. They do **not** depend on React Native or Nitro at runtime, so they can be unit-tested in isolation.
```ts
import {
DEFAULT_ORIGIN_WHITELIST,
createOriginWhitelistGuard,
originMatches,
wrapWithOriginWhitelist,
} from 'nitro-webview'
import type {
OnShouldStartLoadWithRequest,
OriginWhitelistGuard,
} from 'nitro-webview'
```
| Export | Signature | Notes |
| --- | --- | --- |
| `DEFAULT_ORIGIN_WHITELIST` | `readonly ['http://*', 'https://*']` | Frozen. Mirrors `react-native-webview`'s documented default. |
| `originMatches(url, patterns)` | `(string, readonly string[]) => boolean` | Returns `true` iff the **origin** (`scheme://host[:port]`) of `url` matches one of the glob `patterns`. `*` is the only wildcard. Case-insensitive on scheme + host. Empty pattern list returns `false`. Unparseable URL returns `false`. |
| `createOriginWhitelistGuard(patterns?, inner?)` | `(readonly string[], OnShouldStartLoadWithRequest?) => OriginWhitelistGuard` | Builds a guard that rejects non-matching origins immediately and delegates matching ones to `inner` (or allows them when `inner` is absent). |
| `wrapWithOriginWhitelist(handler, patterns?)` | `(OnShouldStartLoadWithRequest, readonly string[]?) => OnShouldStartLoadWithRequest` | Fast-path wrapper: when `patterns === DEFAULT_ORIGIN_WHITELIST` (by reference), the returned guard short-circuits `true` and `handler` is never invoked. Otherwise delegates straight to `handler(event)`. |
```ts
import { wrapWithOriginWhitelist, DEFAULT_ORIGIN_WHITELIST } from 'nitro-webview'
const handler = wrapWithOriginWhitelist(
(event) => !event.url.startsWith('https://example.org/'),
DEFAULT_ORIGIN_WHITELIST,
)
```
### Source helpers
```ts
import {
isHtmlSource,
isUriSource,
normalizeHtmlSource,
sourceToCommand,
} from 'nitro-webview'
```
| Export | Signature | Notes |
| --- | --- | --- |
| `isUriSource(source)` | `(WebViewSource) => source is UriSource` | Structural narrowing on a non-empty `uri` string. |
| `isHtmlSource(source)` | `(WebViewSource) => source is HtmlSource` | Structural narrowing on a string `html` field. |
| `normalizeHtmlSource(source)` | `(WebViewSource) => LoadHtmlCommand \| null` | Returns a `loadHtml` native command, or `null` when `source` is not an `HtmlSource`. |
| `sourceToCommand(source)` | `(WebViewSource) => NativeViewCommand` | Maps the `source` prop to the native view command (`loadUrl` or `loadHtml`). Throws `TypeError` on malformed input. |
### Event dispatchers
Lower-level builders used by `NitroWebView` internally. Exported for advanced consumers building custom event pipelines (e.g. for tests or mocks).
| Export | Signature |
| --- | --- |
| `createLoadStartDispatcher(onLoadStart?)` | `(OnLoadStart \| undefined) => LoadStartDispatcher` |
| `createLoadDispatcher(onLoad?)` | `(OnLoad \| undefined) => LoadDispatcher` |
| `createLoadEndDispatcher(onLoadEnd?)` | `(OnLoadEnd \| undefined) => LoadEndDispatcher` |
Each dispatcher dedupes by `navigationId` so duplicate native fires never reach JS.
### Bridge script
The injected `window.ReactNativeWebView.postMessage(...)` shim is built in pure TS so it can be unit-tested and shared across platforms.
```ts
import {
ANDROID_NATIVE_BRIDGE_NAME,
BRIDGE_NAME,
buildBridgeScript,
evaluateBridgeScript,
} from 'nitro-webview'
```
| Export | Notes |
| --- | --- |
| `BRIDGE_NAME` | `'ReactNativeWebView'`. Public identifier installed on `window`. |
| `ANDROID_NATIVE_BRIDGE_NAME` | `'ReactNativeWebViewNative'`. Internal Android `JavascriptInterface` name. |
| `buildBridgeScript(platform)` | Returns the literal JavaScript source string for the injected bridge. Idempotent β never overwrites a page-defined `postMessage`. |
| `evaluateBridgeScript(platform, sandbox)` | Evaluates the script against an in-memory `sandbox` (used for tests). |
### `callback` re-export
```ts
import { callback } from 'nitro-webview'
```
Re-exported verbatim from `react-native-nitro-modules`. Every event prop (`onLoadStart`, `onLoadEnd`, `onMessage`, `onError`, `onShouldStartLoadWithRequest`, `onFileDownload`, `hybridRef`) must pass through this wrapper.
## Platform setup
### iOS file upload setup
The system file picker on iOS reads from the camera, the photo library, and (for video capture) the microphone. iOS crashes the app the first time the picker accesses one of these subsystems without an explanatory string. Add all three usage descriptions to your app's `Info.plist` even if your web content only triggers one of them β iOS may surface the unified picker:
```xml
NSCameraUsageDescription
This app uses the camera to let you upload photos and videos from web pages.
NSPhotoLibraryUsageDescription
This app needs photo library access to let you upload images from web pages.
NSMicrophoneUsageDescription
This app uses the microphone to record audio when you upload a video from a web page.
```
The strings are shown verbatim in the iOS permission prompt β rewrite them in your app's voice and supported locales.
### Android file upload setup
The library ships its own FileProvider declaration with authority `${applicationId}.nitrowebview.fileprovider`. The consuming app must still declare the media permissions in its `AndroidManifest.xml` for the file chooser to surface photos / videos / camera capture:
```xml
```
The library also pulls `org.mozilla.components:support-utils` for its Content-Dispositionβaware `DownloadUtils.guessFileName` β the consuming app must expose Mozilla's Maven repository in its `android/build.gradle`:
```groovy
allprojects {
repositories {
maven { url "https://maven.mozilla.org/maven2" }
}
}
```
## License
MIT.
[nitro]: https://github.com/mrousavy/nitro