Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 52 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ React Native WebView that auto-sizes to match its HTML content—whether you loa
> [!TIP]
> 💡 Works out-of-the-box with dynamic CMS pages, FAQs, marketing landers, local HTML snippets, or full external sites.

## 🚨 Upgrading to 1.1.0

Three defaults changed in the 1.1.x line. Each is a one-line migration:

- **Named import only.** The default export was removed to keep tree-shaking predictable across bundlers.
```diff
- import SizedWebView from 'react-native-sized-webview';
+ import { SizedWebView } from 'react-native-sized-webview';
```
- **`originWhitelist` now defaults to `['http://*', 'https://*']`.** Standard HTTP(S) navigation keeps working; non-web schemes (`file:`, `javascript:`, `data:`, `intent:`) are blocked by default. Tighten it for production if you only load a specific origin:
```diff
<SizedWebView
+ originWhitelist={['https://your-trusted-domain.com']}
source={{ uri: 'https://your-trusted-domain.com/page' }}
/>
```
- **`javaScriptEnabled` is now respected.** Passing `false` disables auto-sizing; the container falls back to `minHeight` (or `containerStyle.height`). This unblocks rendering static HTML on iOS 26 ([#3](https://github.com/mCodex/react-native-sized-webview/issues/3)).

## ✨ Highlights

- 📐 Wrapper-based measurement keeps the WebView content in a dedicated container, so height always reflects the real DOM footprint.
Expand Down Expand Up @@ -80,14 +98,25 @@ The example showcases:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `minHeight` | `number` | `0` | Minimum height (dp) applied to the container to avoid layout jumps before content loads. |
| `containerStyle` | `StyleProp<ViewStyle>` | — | Styles applied to the wrapping `View`. Use it for padding, borders, or shadows. |
| `onHeightChange` | `(height: number) => void` | — | Callback fired whenever a new height is committed. Great for analytics or debugging. |
| `...WebViewProps` | — | — | All remaining props are forwarded to the underlying `react-native-webview`. |
| `minHeight` | `number` | `0` | Minimum height (dp) applied to the container. When `0`, the container is unsized until the first measurement arrives (avoids layout flicker and the iOS 26 WKWebView 1px feedback loop). |
| `containerStyle` | `StyleProp<ViewStyle>` | — | Styles applied to the wrapping `View`. Use it for padding, borders, or shadows. Do not set `height` — it is managed by the hook. |
| `onHeightChange` | `(height: number) => void` | — | Callback fired whenever a new height is committed. Great for analytics or debugging. Never fires for invalid or out-of-range values. |
| `originWhitelist` | `string[]` | `['http://*', 'https://*']` | Origins the WebView is allowed to navigate to. Blocks non-web schemes (`file:`, `javascript:`, `data:`, `intent:`) by default. Tighten it to a specific origin list for stricter environments. |
| `javaScriptEnabled` | `boolean` | `true` | When `false`, the auto-height bridge is **not** injected and the container falls back to `minHeight`. Use for static HTML that doesn't need JS. |
| `...WebViewProps` | — | — | All remaining props are forwarded to the underlying `react-native-webview`. User-supplied values always win over the defaults above. |

> [!NOTE]
> 🧩 `scrollEnabled` defaults to `false` so sizing remains deterministic. Only enable it if the WebView should manage its own scroll.

## 🛡️ Security

- **Namespaced message protocol.** The injected bridge posts values prefixed with `__RN_SIZED_WV__:` and the hook rejects everything else, so your own `onMessage` traffic cannot accidentally (or maliciously) mutate the container height.
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README security section says the hook "rejects everything else" besides prefixed bridge messages, but the current useAutoHeight implementation still accepts bare numeric strings (and SizedWebView passes all onMessage payloads to it). This makes the namespacing claim inaccurate. Either enforce prefix-only handling for string payloads (recommended) or adjust the README wording to match actual behavior.

Suggested change
- **Namespaced message protocol.** The injected bridge posts values prefixed with `__RN_SIZED_WV__:` and the hook rejects everything else, so your own `onMessage` traffic cannot accidentally (or maliciously) mutate the container height.
- **Namespaced message protocol.** The injected bridge posts values prefixed with `__RN_SIZED_WV__:` so bridge traffic is clearly namespaced. For backward compatibility, the auto-height hook also accepts bare numeric height strings, so custom `onMessage` handlers should avoid sending raw numeric payloads unless they are intended to affect sizing.

Copilot uses AI. Check for mistakes.
- **Safe-by-default origin list.** `originWhitelist` defaults to `['http://*', 'https://*']` — HTTP(S) navigation works, but non-web schemes (`file:`, `javascript:`, `data:`, `intent:`) are blocked. Tighten to a specific origin for production apps that only load trusted content.
- **Respected JS toggle.** `javaScriptEnabled={false}` is honored; the bridge is not injected when you disable scripts.
- **Clamped heights.** A shared `MAX_COMMITTED_HEIGHT` (120 000 dp) caps both sides of the bridge to defend against runaway values from broken markup or third-party scripts.
- **No native code.** This package ships only JavaScript/TypeScript — there is no Objective-C, Swift, Java, or Kotlin to audit.
- **Warning.** Never interpolate untrusted strings into `injectedJavaScript` or `injectedJavaScriptBeforeContentLoaded`. Anything passed there runs inside the WebView page context and can reach React Native through `window.ReactNativeWebView`.

## 🧩 Edge Cases Covered

- Trailing `<br>` and empty `<p>` tags are stripped automatically so CMS exports don’t leave phantom padding.
Expand All @@ -114,6 +143,25 @@ The example showcases:

Benchmarks were captured on CMS articles up to 3k words in a 60 fps RN dev build. The bridge batches DOM mutations so even long documents resize without thrashing the JS thread.

### 🏎️ Built for speed

Every hot path is designed to run at its theoretical complexity floor — no allocations in steady state, no repeated DOM walks, and at most one forced layout per measurement frame.

| Hot path | Complexity | Notes |
| --- | --- | --- |
| Message parsing (`useAutoHeight`) | **O(1)** | Namespaced-prefix check, single `Number()` coerce, constant-bound clamp. |
| Height commit (rAF-batched) | **O(1)** amortized per frame | Sub-pixel diffs are dropped; at most one React render per animation frame. |
| DOM mutation callback | **O(added nodes)** | Scans only each mutation's `addedNodes`, not the whole tree. Media elements are deduped via a `WeakSet`. |
| `measureHeight` | **1 forced reflow / call** | Reads the wrapper element only — its box is authoritative because every `<body>` child lives inside it. |
| Trailing-node prune DFS | Runs only when the DOM is **dirty** | A mutation-driven dirty flag skips the recursive walk on resize / font / viewport ticks when nothing structural changed. |

The net effect: resize storms, font loads, and viewport changes cost a single `getBoundingClientRect()` per frame — nothing more. Paired with `sideEffects: false` and named-only exports, the library stays fast *and* small in the final bundle.

### 📦 Bundle & tree-shaking

- Ships as ESM-first (`lib/module/**`) with `"sideEffects": false`.
- **Named exports only** — no default export — so every bundler can drop what you don't use.
- Importing only `useAutoHeight` or `composeInjectedScript` does **not** pull the injected-bridge string into your bundle.
## ✅ Testing

```sh
Expand Down
99 changes: 99 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.13/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": [
"**/*.js",
"**/*.jsx",
"**/*.ts",
"**/*.tsx",
"**/*.mjs",
"**/*.cjs",
"**/*.json",
"!**/node_modules",
"!**/lib",
"!**/coverage",
"!**/.yarn",
"!example/ios",
"!example/android",
"!example/.expo"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80,
"lineEnding": "lf"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSpacing": true,
"quoteProperties": "asNeeded"
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"useExhaustiveDependencies": "warn",
"useHookAtTopLevel": "error"
},
"style": {
"useConst": "error",
"useTemplate": "error",
"noNonNullAssertion": "warn"
},
"suspicious": {
"noExplicitAny": "warn"
}
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
},
"overrides": [
{
"includes": ["**/__tests__/**", "**/*.test.ts", "**/*.test.tsx"],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
},
"style": {
"noNonNullAssertion": "off"
}
}
}
},
{
"includes": ["src/constants/autoHeightBridge.ts"],
"linter": {
"rules": {
"style": {
"useConst": "off",
"useTemplate": "off"
},
"suspicious": {
"noAssignInExpressions": "off"
}
}
}
}
]
}
29 changes: 0 additions & 29 deletions eslint.config.mjs

This file was deleted.

4 changes: 2 additions & 2 deletions example/babel.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
const path = require('path');
const path = require('node:path');
const { getConfig } = require('react-native-builder-bob/babel-config');
const pkg = require('../package.json');

const root = path.resolve(__dirname, '..');

module.exports = function (api) {
module.exports = (api) => {
api.cache(true);

return getConfig(
Expand Down
2 changes: 1 addition & 1 deletion example/metro.config.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const path = require('path');
const path = require('node:path');
const { getDefaultConfig } = require('@expo/metro-config');
const { withMetroConfig } = require('react-native-monorepo-config');

Expand Down
20 changes: 10 additions & 10 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@
"web": "expo start --web"
},
"dependencies": {
"@expo/metro-runtime": "~6.1.2",
"expo": "~54.0.29",
"expo-status-bar": "~3.0.9",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-safe-area-context": "^5.6.2",
"@expo/metro-runtime": "~55.0.10",
"expo": "~55.0.17",
"expo-status-bar": "~55.0.5",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.6",
"react-native-safe-area-context": "^5.7.0",
"react-native-web": "~0.21.2",
"react-native-webview": "^13.16.0"
"react-native-webview": "^13.16.1"
},
"private": true,
"devDependencies": {
"react-native-builder-bob": "^0.40.17",
"react-native-monorepo-config": "^0.3.1"
"react-native-builder-bob": "^0.41.0",
"react-native-monorepo-config": "^0.3.3"
}
}
2 changes: 1 addition & 1 deletion example/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import {
StyleSheet,
Switch,
Text,
View,
useColorScheme,
View,
} from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { SizedWebView } from 'react-native-sized-webview';
Expand Down
6 changes: 3 additions & 3 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ pre-commit:
parallel: true
commands:
lint:
glob: "*.{js,ts,jsx,tsx}"
run: npx eslint {staged_files}
glob: "*.{js,ts,jsx,tsx,mjs,cjs,json}"
run: npx biome check --no-errors-on-unmatched {staged_files}
types:
glob: "*.{js,ts, jsx, tsx}"
glob: "*.{js,ts,jsx,tsx}"
run: npx tsc
# commit-msg:
# parallel: true
Expand Down
51 changes: 20 additions & 31 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
"example": "yarn workspace react-native-sized-webview-example",
"test": "jest",
"typecheck": "tsc",
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"lint": "biome check",
"lint:fix": "biome check --write",
"format": "biome format --write",
"clean": "del-cli lib",
"prepare": "bob build",
"release": "release-it --only-version"
Expand Down Expand Up @@ -80,33 +82,27 @@
"registry": "https://registry.npmjs.org/"
},
"devDependencies": {
"@commitlint/config-conventional": "^20.2.0",
"@eslint/compat": "^2.0.0",
"@eslint/eslintrc": "^3.3.3",
"@eslint/js": "^9.39.2",
"@evilmartians/lefthook": "^2.0.12",
"@react-native/babel-preset": "0.83.0",
"@react-native/eslint-config": "^0.83.0",
"@release-it/conventional-changelog": "^10.0.3",
"@biomejs/biome": "^2.4.13",
"@commitlint/config-conventional": "^20.5.0",
"@evilmartians/lefthook": "^2.1.6",
"@react-native/babel-preset": "0.83.6",
"@react-native/jest-preset": "0.85.2",
"@release-it/conventional-changelog": "^11.0.0",
"@testing-library/react-native": "^13.3.3",
"@types/jest": "^30.0.0",
"@types/react": "^19.2.7",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"babel-plugin-react-compiler": "^1.0.0",
"commitlint": "^20.2.0",
"commitlint": "^20.5.0",
"del-cli": "^7.0.0",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"jest": "^30.2.0",
"prettier": "^3.7.4",
"react": "19.2.3",
"react-native": "0.83.0",
"react-native-builder-bob": "^0.40.17",
"react-native-webview": "^13.16.0",
"react-test-renderer": "19.2.3",
"release-it": "^19.1.0",
"typescript": "^5.9.3"
"jest": "^30.3.0",
"react": "19.2.0",
"react-native": "0.83.6",
"react-native-builder-bob": "^0.41.0",
"react-native-webview": "^13.16.1",
"react-test-renderer": "19.2.0",
"release-it": "^20.0.1",
"typescript": "^6.0.3"
},
"peerDependencies": {
"react": "*",
Expand All @@ -119,7 +115,7 @@
"sideEffects": false,
"packageManager": "yarn@3.6.1",
"jest": {
"preset": "react-native",
"preset": "@react-native/jest-preset",
"modulePathIgnorePatterns": [
"<rootDir>/example/node_modules",
"<rootDir>/lib/"
Expand Down Expand Up @@ -156,13 +152,6 @@
"release": true
}
},
"prettier": {
"quoteProps": "consistent",
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false
},
"react-native-builder-bob": {
"source": "src",
"output": "lib",
Expand Down
Loading
Loading