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

nitro-webview

v0.1.0

Published

nitro-webview

Readme

nitro-webview

A React Native WebView built on Nitro Modules — 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'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<T> 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<boolean> 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

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

import { NitroWebView, callback } from 'nitro-webview'

export default function Screen() {
  return (
    <NitroWebView
      style={{ flex: 1 }}
      source={{ uri: 'https://example.com' }}
      onLoadEnd={callback(() => 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

import { useRef } from 'react'
import { NitroWebView, callback, type NitroWebViewType } from 'nitro-webview'

export default function Screen() {
  const ref = useRef<NitroWebViewType | null>(null)

  return (
    <>
      <NitroWebView
        style={{ flex: 1 }}
        source={{ uri: 'https://example.com' }}
        hybridRef={callback((r) => {
          ref.current = r
        })}
      />
      <Button title="reload" onPress={() => 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 below.

API Reference

NitroWebView component

The exported React component. Backed by getHostComponent<NitroWebViewProps, NitroWebViewMethods>('NitroWebView', () => NitroWebViewConfig).

Props

| Prop | Type | Notes | | --- | --- | --- | | source | WebViewSource | { uri, headers? } or { html, baseUrl? }. Drives navigation. Required. | | defaultHeaders | Record<string, string> | 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<boolean> | 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<string> | Result is the serialized string evaluation. iOS uses String(describing:); Android uses the JSON-encoded ValueCallback<String> result. Undefined/nil surfaces as ''. | | getCookies(url) | Promise<Cookie[]> | iOS returns the full attribute set. Android CookieManager only exposes name and value on read — other fields are left undefined. | | setCookie(url, cookie) | Promise<void> | Cookie = { name, value, domain?, path?, expires?, secure?, httpOnly? }. expires is milliseconds since epoch (Date.now()-compatible). | | clearCookies() | Promise<void> | Bulk clear via WKWebsiteDataStore (iOS) / CookieManager.removeAllCookies (Android). The promise resolves only after the platform reports completion. |

Types

WebViewSource

type WebViewSource = UriSource | HtmlSource

interface UriSource {
  uri: string
  headers?: Record<string, string>
}

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

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

interface WebViewNavigationState {
  url: string
  title: string
  loading: boolean
  canGoBack: boolean
  canGoForward: boolean
}

interface WebViewLoadEvent {
  nativeEvent: WebViewNavigationState
}

WebViewMessageEvent

interface WebViewMessageNativeEvent {
  data: string  // literal string from window.ReactNativeWebView.postMessage(...)
  url: string
}

interface WebViewMessageEvent {
  nativeEvent: WebViewMessageNativeEvent
}

NitroWebViewErrorEvent

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

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

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.

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). |

import { wrapWithOriginWhitelist, DEFAULT_ORIGIN_WHITELIST } from 'nitro-webview'

const handler = wrapWithOriginWhitelist(
  (event) => !event.url.startsWith('https://example.org/'),
  DEFAULT_ORIGIN_WHITELIST,
)

Source helpers

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.

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

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:

<key>NSCameraUsageDescription</key>
<string>This app uses the camera to let you upload photos and videos from web pages.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs photo library access to let you upload images from web pages.</string>
<key>NSMicrophoneUsageDescription</key>
<string>This app uses the microphone to record audio when you upload a video from a web page.</string>

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:

<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />

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:

allprojects {
  repositories {
    maven { url "https://maven.mozilla.org/maven2" }
  }
}

Known platform limitations

These are inherent to the underlying WebKit / Chromium contracts, not bugs in this library:

  • onShouldStartLoadWithRequest on Android fires only for user-initiated navigations (<a href> taps, form submits, history actions). Programmatic loads — view.loadUrl(...) triggered by changing the source prop from JS — bypass WebViewClient.shouldOverrideUrlLoading and do not invoke the hook. iOS WKNavigationDelegate.decidePolicyFor fires for both programmatic and user-initiated navigations.
  • defaultHeaders and UriSource.headers only apply to the main-frame navigation a source update triggers. Redirects, sub-frames, and sub-resource requests do not re-apply them.
  • onFileDownload never auto-saves. On both platforms the library surfaces the URL + metadata and leaves storage to the JS layer (use @dr.pogodin/react-native-fs, react-native-blob-util, etc.). Blob URLs are out of scope.
  • Android getCookies(url) returns cookies with only name and value populated (the platform CookieManager.getCookie(url) API does not expose the rest). iOS preserves the full attribute set.
  • Android's onShouldStartLoadWithRequest waits up to 250 ms on a synchronized.wait for the JS callback to resolve. If the JS handler does not settle inside that window the navigation defaults to allow (mirrors RNW's SHOULD_OVERRIDE_URL_LOADING_TIMEOUT_MS). iOS has no such timeout.

Example app

The example/ directory contains a bare React Native demo that exercises every feature:

cd example
yarn install
cd ios && pod install
cd ..
yarn start --reset-cache
# In another shell:
yarn ios     # or: yarn android

The demo panels exercise headers, cookies, file upload, file download, user-agent overrides, and navigation interception — each with the platform-specific quirks documented above.

Development

yarn typecheck
yarn lint
yarn test
yarn nitrogen          # regenerate codegen after touching src/specs/*.nitro.ts
swift test             # iOS native unit tests
cd example/android && ./gradlew :nitro-webview:testDebugUnitTest

Style guardrails

The repo ships two layers of automated style enforcement so CLAUDE.md's rules cannot drift silently:

  1. A PostToolUse hook (.claude/settings.json) lints every Edit/Write tool call against scripts/claude-md-guardrails.sh. Claude Code sessions get the same gate by default.

  2. A git pre-commit hook at scripts/git-hooks/pre-commit runs the same script against every staged file. Activate once per clone:

    git config core.hooksPath scripts/git-hooks

Both layers share the same rule set, so adding a rule means editing scripts/claude-md-guardrails.sh and nothing else.

License

MIT.