react-native-instant-webview 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +170 -0
  2. package/lib/commonjs/PooledWebView.js +112 -0
  3. package/lib/commonjs/PooledWebView.js.map +1 -0
  4. package/lib/commonjs/WebViewManager.js +102 -0
  5. package/lib/commonjs/WebViewManager.js.map +1 -0
  6. package/lib/commonjs/WebViewPoolProvider.js +102 -0
  7. package/lib/commonjs/WebViewPoolProvider.js.map +1 -0
  8. package/lib/commonjs/WebViewSlot.js +91 -0
  9. package/lib/commonjs/WebViewSlot.js.map +1 -0
  10. package/lib/commonjs/constants.js +51 -0
  11. package/lib/commonjs/constants.js.map +1 -0
  12. package/lib/commonjs/index.js +41 -0
  13. package/lib/commonjs/index.js.map +1 -0
  14. package/lib/commonjs/types.js +6 -0
  15. package/lib/commonjs/types.js.map +1 -0
  16. package/lib/commonjs/usePooledWebView.js +54 -0
  17. package/lib/commonjs/usePooledWebView.js.map +1 -0
  18. package/lib/module/PooledWebView.js +105 -0
  19. package/lib/module/PooledWebView.js.map +1 -0
  20. package/lib/module/WebViewManager.js +96 -0
  21. package/lib/module/WebViewManager.js.map +1 -0
  22. package/lib/module/WebViewPoolProvider.js +92 -0
  23. package/lib/module/WebViewPoolProvider.js.map +1 -0
  24. package/lib/module/WebViewSlot.js +84 -0
  25. package/lib/module/WebViewSlot.js.map +1 -0
  26. package/lib/module/constants.js +45 -0
  27. package/lib/module/constants.js.map +1 -0
  28. package/lib/module/index.js +5 -0
  29. package/lib/module/index.js.map +1 -0
  30. package/lib/module/types.js +2 -0
  31. package/lib/module/types.js.map +1 -0
  32. package/lib/module/usePooledWebView.js +48 -0
  33. package/lib/module/usePooledWebView.js.map +1 -0
  34. package/lib/typescript/PooledWebView.d.ts +5 -0
  35. package/lib/typescript/PooledWebView.d.ts.map +1 -0
  36. package/lib/typescript/WebViewManager.d.ts +21 -0
  37. package/lib/typescript/WebViewManager.d.ts.map +1 -0
  38. package/lib/typescript/WebViewPoolProvider.d.ts +6 -0
  39. package/lib/typescript/WebViewPoolProvider.d.ts.map +1 -0
  40. package/lib/typescript/WebViewSlot.d.ts +13 -0
  41. package/lib/typescript/WebViewSlot.d.ts.map +1 -0
  42. package/lib/typescript/__mocks__/react-native-webview.d.ts +12 -0
  43. package/lib/typescript/__mocks__/react-native-webview.d.ts.map +1 -0
  44. package/lib/typescript/__mocks__/react-native.d.ts +18 -0
  45. package/lib/typescript/__mocks__/react-native.d.ts.map +1 -0
  46. package/lib/typescript/constants.d.ts +9 -0
  47. package/lib/typescript/constants.d.ts.map +1 -0
  48. package/lib/typescript/index.d.ts +6 -0
  49. package/lib/typescript/index.d.ts.map +1 -0
  50. package/lib/typescript/types.d.ts +62 -0
  51. package/lib/typescript/types.d.ts.map +1 -0
  52. package/lib/typescript/usePooledWebView.d.ts +3 -0
  53. package/lib/typescript/usePooledWebView.d.ts.map +1 -0
  54. package/package.json +87 -0
  55. package/src/PooledWebView.tsx +105 -0
  56. package/src/WebViewManager.ts +120 -0
  57. package/src/WebViewPoolProvider.tsx +138 -0
  58. package/src/WebViewSlot.tsx +107 -0
  59. package/src/__mocks__/react-native-webview.tsx +16 -0
  60. package/src/__mocks__/react-native.ts +14 -0
  61. package/src/constants.ts +46 -0
  62. package/src/index.tsx +17 -0
  63. package/src/types.ts +72 -0
  64. package/src/usePooledWebView.ts +58 -0
@@ -0,0 +1,138 @@
1
+ import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import type { WebViewProps } from 'react-native-webview';
4
+ import { DEFAULT_POOL_CONFIG } from './constants';
5
+ import WebViewManager from './WebViewManager';
6
+ import WebViewSlot from './WebViewSlot';
7
+ import type {
8
+ BorrowResult,
9
+ InstanceLayout,
10
+ PoolConfig,
11
+ PoolState,
12
+ WebViewPoolContextValue,
13
+ WebViewPoolProviderProps,
14
+ } from './types';
15
+
16
+ const WebViewPoolContext = createContext<WebViewPoolContextValue | null>(null);
17
+
18
+ export function useWebViewPool(): WebViewPoolContextValue {
19
+ const ctx = useContext(WebViewPoolContext);
20
+ if (!ctx) {
21
+ throw new Error('useWebViewPool must be used within a WebViewPoolProvider');
22
+ }
23
+ return ctx;
24
+ }
25
+
26
+ export const WebViewPoolProvider: React.FC<WebViewPoolProviderProps> = ({
27
+ config,
28
+ children,
29
+ }) => {
30
+ const managerRef = useRef(WebViewManager.getInstance());
31
+ const mergedConfig = useRef<PoolConfig>({
32
+ ...DEFAULT_POOL_CONFIG,
33
+ ...config,
34
+ }).current;
35
+
36
+ const [poolState, setPoolState] = useState<PoolState>(() => {
37
+ const mgr = managerRef.current;
38
+ mgr.initialize(mergedConfig);
39
+ return mgr.getState();
40
+ });
41
+
42
+ const layoutsRef = useRef<Map<string, InstanceLayout | null>>(new Map());
43
+ const propsRef = useRef<Map<string, Partial<WebViewProps>>>(new Map());
44
+
45
+ const [, forceRender] = useState(0);
46
+
47
+ useEffect(() => {
48
+ const mgr = managerRef.current;
49
+ const unsub = mgr.subscribe((state) => {
50
+ setPoolState(state);
51
+ });
52
+ return unsub;
53
+ }, []);
54
+
55
+ const borrow = useCallback(
56
+ (borrowerId: string): BorrowResult | null => {
57
+ return managerRef.current.borrow(borrowerId);
58
+ },
59
+ [],
60
+ );
61
+
62
+ const release = useCallback((instanceId: string): void => {
63
+ layoutsRef.current.delete(instanceId);
64
+ propsRef.current.delete(instanceId);
65
+ managerRef.current.release(instanceId);
66
+ }, []);
67
+
68
+ const setInstanceLayout = useCallback(
69
+ (instanceId: string, layout: InstanceLayout | null): void => {
70
+ layoutsRef.current.set(instanceId, layout);
71
+ forceRender((c) => c + 1);
72
+ },
73
+ [],
74
+ );
75
+
76
+ const setInstanceProps = useCallback(
77
+ (instanceId: string, props: Partial<WebViewProps>): void => {
78
+ propsRef.current.set(instanceId, props);
79
+ // No forceRender here — the slot reads from propsRef on next render
80
+ // triggered by the Manager's state change (borrow/release).
81
+ },
82
+ [],
83
+ );
84
+
85
+ const getInstanceLayout = useCallback(
86
+ (instanceId: string): InstanceLayout | null => {
87
+ return layoutsRef.current.get(instanceId) ?? null;
88
+ },
89
+ [],
90
+ );
91
+
92
+ const getInstanceProps = useCallback(
93
+ (instanceId: string): Partial<WebViewProps> | undefined => {
94
+ return propsRef.current.get(instanceId);
95
+ },
96
+ [],
97
+ );
98
+
99
+ const handleCleanupComplete = useCallback((instanceId: string) => {
100
+ managerRef.current.markIdle(instanceId);
101
+ }, []);
102
+
103
+ const contextValue: WebViewPoolContextValue = {
104
+ state: poolState,
105
+ borrow,
106
+ release,
107
+ setInstanceLayout,
108
+ setInstanceProps,
109
+ getInstanceLayout,
110
+ getInstanceProps,
111
+ };
112
+
113
+ return (
114
+ <WebViewPoolContext.Provider value={contextValue}>
115
+ <View style={styles.container}>
116
+ {children}
117
+ {poolState.instances.map((instance) => (
118
+ <WebViewSlot
119
+ key={instance.id}
120
+ instance={instance}
121
+ layout={layoutsRef.current.get(instance.id) ?? null}
122
+ instanceProps={propsRef.current.get(instance.id)}
123
+ config={mergedConfig}
124
+ onCleanupComplete={handleCleanupComplete}
125
+ />
126
+ ))}
127
+ </View>
128
+ </WebViewPoolContext.Provider>
129
+ );
130
+ };
131
+
132
+ const styles = StyleSheet.create({
133
+ container: {
134
+ flex: 1,
135
+ },
136
+ });
137
+
138
+ export default WebViewPoolProvider;
@@ -0,0 +1,107 @@
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import { View, type ViewStyle } from 'react-native';
3
+ import { WebView, type WebViewNavigation } from 'react-native-webview';
4
+ import { BLANK_HTML_SOURCE, CLEANUP_SCRIPT, HIDDEN_STYLE } from './constants';
5
+ import type { InstanceLayout, PoolConfig, WebViewInstance } from './types';
6
+ import type { WebViewProps } from 'react-native-webview';
7
+
8
+ interface WebViewSlotProps {
9
+ instance: WebViewInstance;
10
+ layout: InstanceLayout | null;
11
+ instanceProps: Partial<WebViewProps> | undefined;
12
+ config: PoolConfig;
13
+ onCleanupComplete: (instanceId: string) => void;
14
+ }
15
+
16
+ const WebViewSlot: React.FC<WebViewSlotProps> = ({
17
+ instance,
18
+ layout,
19
+ instanceProps,
20
+ config,
21
+ onCleanupComplete,
22
+ }) => {
23
+ const isVisible = instance.status === 'borrowed' && layout != null;
24
+ const prevStatusRef = useRef(instance.status);
25
+
26
+ // Track whether this slot has ever had a WebView rendered.
27
+ // On first borrow the WebView is created with a valid user source,
28
+ // avoiding the Fabric crash where didMoveToWindow fires before
29
+ // the source prop is applied (causing loadFileURL: with nil URL).
30
+ // After the first borrow, the WebView stays alive through
31
+ // cleaning → idle cycles.
32
+ const [hasWebView, setHasWebView] = useState(false);
33
+
34
+ useEffect(() => {
35
+ if (instance.status === 'borrowed' && !hasWebView) {
36
+ setHasWebView(true);
37
+ }
38
+ }, [instance.status, hasWebView]);
39
+
40
+ // When entering cleaning state, inject cleanup script then mark idle
41
+ useEffect(() => {
42
+ if (prevStatusRef.current !== 'cleaning' && instance.status === 'cleaning') {
43
+ const ref = instance.webViewRef.current;
44
+ if (ref) {
45
+ const script = config.customCleanupScript ?? CLEANUP_SCRIPT;
46
+ ref.injectJavaScript(script);
47
+ }
48
+ const timer = setTimeout(() => {
49
+ onCleanupComplete(instance.id);
50
+ }, 100);
51
+ return () => clearTimeout(timer);
52
+ }
53
+ prevStatusRef.current = instance.status;
54
+ }, [instance.status, instance.id, instance.webViewRef, config.customCleanupScript, onCleanupComplete]);
55
+
56
+ const containerStyle = useMemo<ViewStyle>(() => {
57
+ if (!isVisible || !layout) {
58
+ return HIDDEN_STYLE;
59
+ }
60
+ return {
61
+ position: 'absolute',
62
+ top: layout.top,
63
+ left: layout.left,
64
+ width: layout.width,
65
+ height: layout.height,
66
+ };
67
+ }, [isVisible, layout]);
68
+
69
+ const handleNavigationStateChange = useCallback(
70
+ (navState: WebViewNavigation) => {
71
+ instanceProps?.onNavigationStateChange?.(navState);
72
+ },
73
+ [instanceProps],
74
+ );
75
+
76
+ const source = useMemo(() => {
77
+ if (instance.status === 'borrowed' && instanceProps?.source) {
78
+ return instanceProps.source;
79
+ }
80
+ return BLANK_HTML_SOURCE;
81
+ }, [instance.status, instanceProps?.source]);
82
+
83
+ // Don't render WebView until the first borrow.
84
+ // This avoids the Fabric crash where didMoveToWindow → visitSource
85
+ // fires before _source prop is applied on the native side.
86
+ const shouldRenderWebView = hasWebView;
87
+
88
+ return (
89
+ <View
90
+ style={containerStyle}
91
+ pointerEvents={isVisible ? 'auto' : 'none'}
92
+ >
93
+ {shouldRenderWebView && (
94
+ <WebView
95
+ ref={instance.webViewRef as React.RefObject<WebView>}
96
+ {...(instance.status === 'borrowed' ? instanceProps : undefined)}
97
+ source={source}
98
+ onNavigationStateChange={handleNavigationStateChange}
99
+ style={{ flex: 1 }}
100
+ {...(config.defaultWebViewProps || {})}
101
+ />
102
+ )}
103
+ </View>
104
+ );
105
+ };
106
+
107
+ export default React.memo(WebViewSlot);
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+
3
+ const WebView = React.forwardRef((props: any, ref: any) => {
4
+ return React.createElement('WebView', { ...props, testID: props.testID ?? 'mock-webview', ref });
5
+ });
6
+ WebView.displayName = 'WebView';
7
+
8
+ export { WebView };
9
+ export type WebViewProps = Record<string, any>;
10
+ export type WebViewNavigation = {
11
+ url: string;
12
+ title: string;
13
+ loading: boolean;
14
+ canGoBack: boolean;
15
+ canGoForward: boolean;
16
+ };
@@ -0,0 +1,14 @@
1
+ import React from 'react';
2
+
3
+ const View = React.forwardRef(({ children, testID, ...props }: any, ref: any) => {
4
+ return React.createElement('View', { ...props, testID, ref }, children);
5
+ });
6
+ View.displayName = 'View';
7
+
8
+ const StyleSheet = {
9
+ create: (styles: any) => styles,
10
+ };
11
+
12
+ export { View, StyleSheet };
13
+ export type ViewStyle = Record<string, any>;
14
+ export type LayoutChangeEvent = { nativeEvent: { layout: { x: number; y: number; width: number; height: number } } };
@@ -0,0 +1,46 @@
1
+ import type { ViewStyle } from 'react-native';
2
+ import type { PoolConfig } from './types';
3
+
4
+ export const DEFAULT_POOL_CONFIG: PoolConfig = {
5
+ poolSize: 3,
6
+ cleanupOnReturn: true,
7
+ };
8
+
9
+ export const HIDDEN_STYLE: ViewStyle = {
10
+ position: 'absolute',
11
+ width: 1,
12
+ height: 1,
13
+ left: -9999,
14
+ top: -9999,
15
+ opacity: 0,
16
+ };
17
+
18
+ // Blank HTML source used for idle/cleaning WebViews.
19
+ // Using { html } instead of { uri: 'about:blank' } because
20
+ // react-native-webview's native code may route 'about:blank'
21
+ // through loadFileURL: which throws NSInvalidArgumentException.
22
+ export const BLANK_HTML_SOURCE = { html: '' } as const;
23
+
24
+ // Cleanup script injected into WebView when returning to pool.
25
+ // Resets scroll position, clears timers, and removes all body content.
26
+ // The body.innerHTML = '' is intentional — it clears the WebView DOM
27
+ // as part of the pool cleanup process (no user content involved).
28
+ export const CLEANUP_SCRIPT = `
29
+ (function() {
30
+ try {
31
+ window.scrollTo(0, 0);
32
+ var highestTimeoutId = setTimeout(function(){}, 0);
33
+ for (var i = 0; i < highestTimeoutId; i++) {
34
+ clearTimeout(i);
35
+ }
36
+ var highestIntervalId = setInterval(function(){}, 0);
37
+ for (var j = 0; j < highestIntervalId; j++) {
38
+ clearInterval(j);
39
+ }
40
+ if (document.body) {
41
+ document.body.innerHTML = '';
42
+ }
43
+ } catch(e) {}
44
+ true;
45
+ })();
46
+ `;
package/src/index.tsx ADDED
@@ -0,0 +1,17 @@
1
+ export { WebViewPoolProvider, useWebViewPool } from './WebViewPoolProvider';
2
+ export { default as PooledWebView } from './PooledWebView';
3
+ export { usePooledWebView } from './usePooledWebView';
4
+ export { default as WebViewManager } from './WebViewManager';
5
+
6
+ export type {
7
+ PoolConfig,
8
+ InstanceStatus,
9
+ WebViewInstance,
10
+ PoolState,
11
+ BorrowResult,
12
+ WebViewPoolContextValue,
13
+ InstanceLayout,
14
+ PooledWebViewProps,
15
+ WebViewPoolProviderProps,
16
+ UsePooledWebViewReturn,
17
+ } from './types';
package/src/types.ts ADDED
@@ -0,0 +1,72 @@
1
+ import type { RefObject } from 'react';
2
+ import type { StyleProp, ViewStyle } from 'react-native';
3
+ import type { WebView, WebViewProps } from 'react-native-webview';
4
+
5
+ export interface PoolConfig {
6
+ poolSize: number;
7
+ cleanupOnReturn: boolean;
8
+ customCleanupScript?: string;
9
+ defaultWebViewProps?: Partial<WebViewProps>;
10
+ }
11
+
12
+ export type InstanceStatus = 'idle' | 'borrowed' | 'cleaning';
13
+
14
+ export interface WebViewInstance {
15
+ id: string;
16
+ status: InstanceStatus;
17
+ webViewRef: RefObject<WebView | null>;
18
+ borrowerId: string | null;
19
+ createdAt: number;
20
+ borrowedAt: number | null;
21
+ }
22
+
23
+ export interface PoolState {
24
+ instances: WebViewInstance[];
25
+ availableCount: number;
26
+ borrowedCount: number;
27
+ initialized: boolean;
28
+ }
29
+
30
+ export interface BorrowResult {
31
+ instanceId: string;
32
+ webViewRef: RefObject<WebView | null>;
33
+ }
34
+
35
+ export interface InstanceLayout {
36
+ top: number;
37
+ left: number;
38
+ width: number;
39
+ height: number;
40
+ }
41
+
42
+ export interface WebViewPoolContextValue {
43
+ state: PoolState;
44
+ borrow: (borrowerId: string) => BorrowResult | null;
45
+ release: (instanceId: string) => void;
46
+ setInstanceLayout: (instanceId: string, layout: InstanceLayout | null) => void;
47
+ setInstanceProps: (instanceId: string, props: Partial<WebViewProps>) => void;
48
+ getInstanceLayout: (instanceId: string) => InstanceLayout | null;
49
+ getInstanceProps: (instanceId: string) => Partial<WebViewProps> | undefined;
50
+ }
51
+
52
+ export interface PooledWebViewProps extends Omit<WebViewProps, 'ref'> {
53
+ poolKey?: string;
54
+ containerStyle?: StyleProp<ViewStyle>;
55
+ onPoolExhausted?: () => void;
56
+ onBorrowed?: (instanceId: string) => void;
57
+ onReturned?: (instanceId: string) => void;
58
+ }
59
+
60
+ export interface WebViewPoolProviderProps {
61
+ config?: Partial<PoolConfig>;
62
+ children: React.ReactNode;
63
+ }
64
+
65
+ export interface UsePooledWebViewReturn {
66
+ borrow: () => BorrowResult | null;
67
+ release: () => void;
68
+ instanceId: string | null;
69
+ webViewRef: RefObject<WebView | null> | null;
70
+ }
71
+
72
+ export type PoolListener = (state: PoolState) => void;
@@ -0,0 +1,58 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import type { RefObject } from 'react';
3
+ import type { WebView } from 'react-native-webview';
4
+ import { useWebViewPool } from './WebViewPoolProvider';
5
+ import type { UsePooledWebViewReturn } from './types';
6
+
7
+ let hookBorrowerIdCounter = 0;
8
+
9
+ export function usePooledWebView(): UsePooledWebViewReturn {
10
+ const pool = useWebViewPool();
11
+ const instanceIdRef = useRef<string | null>(null);
12
+ const webViewRefRef = useRef<RefObject<WebView | null> | null>(null);
13
+ const [instanceId, setInstanceId] = useState<string | null>(null);
14
+
15
+ useEffect(() => {
16
+ return () => {
17
+ if (instanceIdRef.current) {
18
+ pool.release(instanceIdRef.current);
19
+ instanceIdRef.current = null;
20
+ webViewRefRef.current = null;
21
+ }
22
+ };
23
+ }, [pool]);
24
+
25
+ const borrow = useCallback(() => {
26
+ if (instanceIdRef.current) {
27
+ return {
28
+ instanceId: instanceIdRef.current,
29
+ webViewRef: webViewRefRef.current!,
30
+ };
31
+ }
32
+
33
+ const borrowerId = `hook-borrower-${++hookBorrowerIdCounter}`;
34
+ const result = pool.borrow(borrowerId);
35
+ if (!result) return null;
36
+
37
+ instanceIdRef.current = result.instanceId;
38
+ webViewRefRef.current = result.webViewRef;
39
+ setInstanceId(result.instanceId);
40
+ return result;
41
+ }, [pool]);
42
+
43
+ const release = useCallback(() => {
44
+ if (instanceIdRef.current) {
45
+ pool.release(instanceIdRef.current);
46
+ instanceIdRef.current = null;
47
+ webViewRefRef.current = null;
48
+ setInstanceId(null);
49
+ }
50
+ }, [pool]);
51
+
52
+ return {
53
+ borrow,
54
+ release,
55
+ instanceId,
56
+ webViewRef: webViewRefRef.current,
57
+ };
58
+ }