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/react@0.51.4 and still present in the code of 0.54.0 (with @tiptap/core@3.26.1 / 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 →
childList mutation, added nodes contentEditable
ignoreMutation returns false → ProseMirror redraws the node view
- React remounts → another
childList mutation → 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
createReactBlockSpec block 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+1 in 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
ignoreMutation option from createReactBlockSpec through to ReactNodeViewRenderer, or
- have BlockNote's renderer supply a default
ignoreMutation that ignores mutations originating from the React-rendered part of the node view (everything outside contentDOM).
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.)
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/react@0.51.4and still present in the code of0.54.0(with@tiptap/core@3.26.1/3.30.2).Mechanism
@tiptap/core'sNodeView.ignoreMutationhas an iOS/Android branch (added for ueberdosis/tiptap#1971):this.domis the whole node view, so React mounting the block's own rendered content inside it also matches:childListmutation, added nodes contentEditableignoreMutationreturnsfalse→ ProseMirror redraws the node viewchildListmutation → back to 2, foreverThe 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 byappendChild/removeChild,ignoreMutation, and the React node-view teardown/rebuild cycle.Why users can't work around it
TipTap consults
this.options.ignoreMutationbefore the iOS branch — so a per-node-view override would fix it cleanly. But@blocknote/reacthardcodes the renderer options:createReactBlockSpecexposes no way to passignoreMutationthrough (same gap as ueberdosis/tiptap#1538). The only fix available today is patching@tiptap/core(we shippatch-packagenarrowing the branch'sthis.domtothis.contentDOM, which preserves the mobile-keyboard fix the branch exists for while excluding node-view chrome).Reproduction (no device needed)
TipTap's
isiOS()isplatform ∈ {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. CDPEmulation.setTouchEmulationEnabledor DevTools device toolbar):createReactBlockSpecblock registered, touch emulation on1+1in the console never evaluatesNeither viewport width nor the mobile UA matters; touch capability alone flips
isiOS()'s second clause. On a real iPhonenavigator.platform === "iPhone"makes it unconditional.Ask
Either:
ignoreMutationoption fromcreateReactBlockSpecthrough toReactNodeViewRenderer, orignoreMutationthat 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.domvsthis.contentDOMquestion with TipTap, but BlockNote exposing the seam is what lets consumers fix this without vendor patches.)