Skip to content

iOS: inserting any createReactBlockSpec block freezes the tab (infinite mutation⇄re-render loop; no ignoreMutation seam to fix it) #2994

Description

@marxjb

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:

  1. React mounts the block component inside the node view → childList mutation, added nodes contentEditable
  2. ignoreMutation returns false → ProseMirror redraws the node view
  3. 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):

  1. Editor with any createReactBlockSpec block registered, touch emulation on
  2. Focus the editor, open the slash menu, insert the React block (keyboard Enter suffices — no tap involved)
  3. 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.)

Activity

  1. marxjb commented on Aug 20, 2026

    @marxjb
    Author

    Filed the underlying TipTap report: ueberdosis/tiptap#8220 (proposes narrowing the branch to contentDOM, which is the patch we run in production).

  2. marxjb commented on Aug 20, 2026

    @marxjb
    Author

    Runnable minimal reproduction: https://github.com/marxjb/blocknote-ios-freeze-repro

    npm install && npm run repro runs the app headless twice (touch emulation off / on), presses the same insert button, and asks the renderer whether it still evaluates 1+1:

    touch OFF: OK (renderer responsive)
    touch ON:  FROZEN (renderer unresponsive >8s)
    

    One finding worth flagging while building it — measured, same harness, @tiptap/core/@tiptap/react identical at 3.30.2 in both rows:

    @blocknote/react touch ON
    0.51.4 (repo pins this) FROZEN
    0.54.0 OK

    So something in BlockNote 0.52–0.54 changed the React node-view mounting such that this minimal case no longer triggers the TipTap branch (the branch itself is unchanged through 3.30.2). The harness refuses to report OK unless the inserted block actually rendered, so the 0.54 pass isn't a silently failed insertion.

    That may reduce the practical severity on latest — though I don't know which change did it or whether richer blocks (portals, popovers, effects that touch the DOM on mount) still trigger it on 0.54. The ignoreMutation seam request stands either way: the hazardous TipTap branch is still live, and consumers on it have no non-patch recourse. If someone recognizes which 0.52–0.54 change altered the mount behaviour, that would tell us whether the fix was deliberate or incidental.

  3. matthewlipski commented on Aug 21, 2026

    @matthewlipski
    Collaborator

    Hi @marxjb, it seems that #2912 should be the appropriate fix for this issue and was included in the 0.53.0 release. Could you verify whether you're still getting a crash on that version or above?

  4. xiaweiss commented on Sep 18, 2026

    @xiaweiss

    Why does a block node have two layers of nodeViewWrapper?

    Image
  5. xiaweiss commented on Sep 18, 2026

    @xiaweiss

    This looks like a nesting-induced bug — up until the loop kicks in, ignoreMutation returns true as expected.

    Details are over in tiptap#8220 — I commented there.

  6. matthewlipski commented on Sep 18, 2026

    @matthewlipski
    Collaborator

    Why does a block node have two layers of nodeViewWrapper?

    Image

    This is just how TipTap renders React node views with content - you get a data-node-view-wrapper on the node view's root element, then the content is wrapped in both its own data-node-view-wrapper and a parent data-node-view-content. Works the same in their draggable block example.

    @xiaweiss out of curiosity, which versions of BlockNote/Tiptap have you reproduced this issue in? Just to know if #2912 is relevant or if it's strictly down to ueberdosis/tiptap#8220

  7. xiaweiss commented on Sep 18, 2026

    @xiaweiss

    Reproduced on v1.31.1 — it affects mobile line-breaking behaviour. I have a fix working in the current example; it might be worth exercising other BlockNote scenarios too.

  8. xiaweiss commented on Sep 18, 2026

    @xiaweiss
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions