iOS: inserting any `createReactBlockSpec` block freezes the tab (infinite mutation⇄re-render loop; no `ignoreMutation` seam to fix it)
Summary
On iOS/iPadOS Safari, inserting a custom React block (createReactBlockSpec) into the editor while it is focused locks the main thread in an infinite loop. The tab freezes with no exception and no console output; iOS then kills it. Native blocks (paragraph, quote, heading…) are unaffected.
Verified on @blocknote/[email protected] and still present in the code of 0.54.0 (with @tiptap/[email protected] / 3.30.2).
Mechanism
@tiptap/core's NodeView.ignoreMutation has an iOS/Android branch (added for ueberdosis/tiptap#1971):
if (this.dom.contains(mutation.target) && mutation.type === "childList"
&& (isiOS() || isAndroid()) && this.editor.isFocused) {
const changedNodes = [...mutation.addedNodes, ...mutation.removedNodes];
if (changedNodes.every((node) => node.isContentEditable)) return false; // "do NOT ignore"
}this.dom is the whole node view, so React mounting the block's own rendered content inside it also matches:
- React mounts the block component inside the node view →
childListmutation, added nodes contentEditable ignoreMutationreturnsfalse→ ProseMirror redraws the node view- React remounts → another
childListmutation → back to 2, forever
The loop is synchronous and reschedules in a microtask (processRootScheduleInMicrotask → flushSyncWorkAcrossRoots_impl → performSyncWorkOnRoot), so the thread never yields. A CPU profile at the freeze is dominated by appendChild/removeChild, ignoreMutation, and the React node-view teardown/rebuild cycle.
Why users can't work around it
TipTap consults this.options.ignoreMutation before the iOS branch — so a per-node-view override would fix it cleanly. But @blocknote/react hardcodes the renderer options:
ReactNodeViewRenderer(component, { className: "bn-react-node-view-renderer" })createReactBlockSpec exposes no way to pass ignoreMutation through (same gap as ueberdosis/tiptap#1538). The only fix available today is patching @tiptap/core (we ship patch-package narrowing the branch's this.dom to this.contentDOM, which preserves the mobile-keyboard fix the branch exists for while excluding node-view chrome).
Reproduction (no device needed)
TipTap's isiOS() is platform ∈ {iPad, iPhone, iPod, …} || (navigator.userAgent.includes("Mac") && "ontouchend" in document) — so Chrome/Chromium with touch emulation enabled and any Mac-containing UA reproduces it exactly (e.g. CDP Emulation.setTouchEmulationEnabled or DevTools device toolbar):
- Editor with any
createReactBlockSpecblock registered, touch emulation on - Focus the editor, open the slash menu, insert the React block (keyboard Enter suffices — no tap involved)
- Main thread wedges:
1+1in the console never evaluates
Neither viewport width nor the mobile UA matters; touch capability alone flips isiOS()'s second clause. On a real iPhone navigator.platform === "iPhone" makes it unconditional.
Ask
Either:
- forward an
ignoreMutationoption fromcreateReactBlockSpecthrough toReactNodeViewRenderer, or - have BlockNote's renderer supply a default
ignoreMutationthat ignores mutations originating from the React-rendered part of the node view (everything outsidecontentDOM).
Happy to PR either direction if you have a preference. (We're also filing the underlying this.dom vs this.contentDOM question with TipTap, but BlockNote exposing the seam is what lets consumers fix this without vendor patches.)
Source: TypeCellOS/BlockNote