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.
- package/README.md +170 -0
- package/lib/commonjs/PooledWebView.js +112 -0
- package/lib/commonjs/PooledWebView.js.map +1 -0
- package/lib/commonjs/WebViewManager.js +102 -0
- package/lib/commonjs/WebViewManager.js.map +1 -0
- package/lib/commonjs/WebViewPoolProvider.js +102 -0
- package/lib/commonjs/WebViewPoolProvider.js.map +1 -0
- package/lib/commonjs/WebViewSlot.js +91 -0
- package/lib/commonjs/WebViewSlot.js.map +1 -0
- package/lib/commonjs/constants.js +51 -0
- package/lib/commonjs/constants.js.map +1 -0
- package/lib/commonjs/index.js +41 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/usePooledWebView.js +54 -0
- package/lib/commonjs/usePooledWebView.js.map +1 -0
- package/lib/module/PooledWebView.js +105 -0
- package/lib/module/PooledWebView.js.map +1 -0
- package/lib/module/WebViewManager.js +96 -0
- package/lib/module/WebViewManager.js.map +1 -0
- package/lib/module/WebViewPoolProvider.js +92 -0
- package/lib/module/WebViewPoolProvider.js.map +1 -0
- package/lib/module/WebViewSlot.js +84 -0
- package/lib/module/WebViewSlot.js.map +1 -0
- package/lib/module/constants.js +45 -0
- package/lib/module/constants.js.map +1 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/usePooledWebView.js +48 -0
- package/lib/module/usePooledWebView.js.map +1 -0
- package/lib/typescript/PooledWebView.d.ts +5 -0
- package/lib/typescript/PooledWebView.d.ts.map +1 -0
- package/lib/typescript/WebViewManager.d.ts +21 -0
- package/lib/typescript/WebViewManager.d.ts.map +1 -0
- package/lib/typescript/WebViewPoolProvider.d.ts +6 -0
- package/lib/typescript/WebViewPoolProvider.d.ts.map +1 -0
- package/lib/typescript/WebViewSlot.d.ts +13 -0
- package/lib/typescript/WebViewSlot.d.ts.map +1 -0
- package/lib/typescript/__mocks__/react-native-webview.d.ts +12 -0
- package/lib/typescript/__mocks__/react-native-webview.d.ts.map +1 -0
- package/lib/typescript/__mocks__/react-native.d.ts +18 -0
- package/lib/typescript/__mocks__/react-native.d.ts.map +1 -0
- package/lib/typescript/constants.d.ts +9 -0
- package/lib/typescript/constants.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +62 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/usePooledWebView.d.ts +3 -0
- package/lib/typescript/usePooledWebView.d.ts.map +1 -0
- package/package.json +87 -0
- package/src/PooledWebView.tsx +105 -0
- package/src/WebViewManager.ts +120 -0
- package/src/WebViewPoolProvider.tsx +138 -0
- package/src/WebViewSlot.tsx +107 -0
- package/src/__mocks__/react-native-webview.tsx +16 -0
- package/src/__mocks__/react-native.ts +14 -0
- package/src/constants.ts +46 -0
- package/src/index.tsx +17 -0
- package/src/types.ts +72 -0
- 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 } } };
|
package/src/constants.ts
ADDED
|
@@ -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
|
+
}
|