-
Notifications
You must be signed in to change notification settings - Fork 78
Description
Describe the bug
On Fabric (New Architecture) with react-native-webview 13.15.0, the TenTap editor intermittently (and in our case, consistently) never becomes ready. useBridgeState(editor).isReady stays false permanently, editorState.empty stays true, and the WebView shows a blank page.
The root cause is a race condition between injectedJavaScriptBeforeContentLoaded and the WebView's HTML loading on Fabric. The HTML's setInterval(..., 1) polls for window.contentInjected before initializing TipTap — but on Fabric, the source prop (HTML content) can be applied before injectedJavaScriptBeforeContentLoaded executes. When this happens, window.contentInjected is never set, TipTap never initializes, and no StateUpdate/EditorReady messages are sent to React Native.
Related: #335 (same symptom, closed but the underlying Fabric timing issue is unaddressed)
Environment
- @10play/tentap-editor: 1.0.1
- react-native: 0.81.5 (New Architecture / Fabric enabled)
- react-native-webview: 13.15.0 (Expo SDK 54 pin)
- expo: ~54.0.x
- Platform: iOS (confirmed), Android (not yet tested)
To Reproduce
- Use TenTap 1.0.1 with React Native 0.81.5 and Fabric enabled
- Render a
<RichText editor={editor} />component - Observe that
useBridgeState(editor).isReadystaysfalse
This reproduces consistently in our app after upgrading from RN 0.79.5 (Old Architecture) to 0.81.5 (Fabric). It worked on every version prior to the New Architecture migration.
Root cause analysis
The initialization chain
RichText.tsxpassesinjectedJavaScriptBeforeContentLoaded={getInjectedJSBeforeContentLoad(editor)}to the WebViewgetInjectedJSBeforeContentLoad()(insrc/RichText/utils.ts) setswindow.bridgeExtensionConfigMap,window.whiteListBridgeExtensions,window.editable,window.contentInjected = true, etc.- The editor HTML contains a
setInterval(() => { ... }, 1)that polls forwindow.contentInjectedbefore calling the TipTap initialization code - Once TipTap initializes, it posts
StateUpdateandEditorReadymessages back to React Native
What breaks on Fabric
On Fabric, native view creation and prop application are not guaranteed to be synchronous in the same way as the old architecture. The source prop (containing the HTML) may be applied to WKWebView before the injectedJavaScriptBeforeContentLoaded user script is registered. When this happens:
- The HTML loads and starts executing JavaScript
setIntervalpolls forwindow.contentInjected— it'sundefined- The poll continues forever, TipTap never initializes
- No
EditorReadymessage is ever sent
Additional complication: the key-change remount
RichText.tsx lines 149-155 force a WebView remount on iOS by changing the key prop from 'webview' to 'webview_reloaded' on first load. This was a workaround for react-native-webview#3578, which was fixed in react-native-webview 13.12.5. Since 13.15.0 already contains the fix, this remount is unnecessary and may exacerbate timing issues on Fabric by creating a second WebView instance that also needs to race injectedJavaScriptBeforeContentLoaded against HTML loading.
Workaround
We worked around this by re-injecting the initialization variables via injectJavaScript() in the onLoad callback as a fallback:
<RichText
editor={editor}
onLoad={() => {
// Fallback for Fabric race condition:
// injectedJavaScriptBeforeContentLoaded may not execute before the HTML
// loads, leaving window.contentInjected unset and TipTap stuck.
setTimeout(() => {
const initJS = getInjectedJSBeforeContentLoad(editor);
editor.injectJS(
`if (!window.contentInjected) { ${initJS} } true;`,
);
}, 50);
}}
/>This re-injects window.contentInjected = true (and all other bridge config variables) after the WebView reports it has loaded. The if (!window.contentInjected) guard makes it a no-op when the original injection worked correctly. The HTML's setInterval(..., 1) picks up the variables on the next tick.
This works but is fragile — it depends on importing getInjectedJSBeforeContentLoad from TenTap's internal src/RichText/utils and duplicates logic that should live in RichText.tsx itself.
Suggested fix in TenTap
The fix should live inside RichText.tsx's onLoad handler. After TenTap's existing onLoad logic runs, it should call editor.webviewRef.current.injectJavaScript() with the same content as getInjectedJSBeforeContentLoad(editor), guarded by if (!window.contentInjected).
Additionally, consider removing the key-change remount workaround (lines 149-155) since react-native-webview 13.12.5+ already contains the fix for #3578, and the remount adds unnecessary complexity on Fabric.
// Inside RichText.tsx onLoad handler:
onLoad={(e) => {
setLoaded(true);
// Existing iOS key-change workaround (consider removing)
if (Platform.OS === 'ios' && key === 'webview') {
setKey('webview_reloaded');
}
// Fallback: re-inject init variables in case injectedJavaScriptBeforeContentLoaded
// lost the race against HTML loading on Fabric
setTimeout(() => {
editor.webviewRef.current?.injectJavaScript(
`if (!window.contentInjected) { ${getInjectedJSBeforeContentLoad(editor)} } true;`
);
}, 50);
props.onLoad && props.onLoad(e);
}}