Custom block React state silently resets when ProseMirror recreates the node view (external DOM writes read as content changes)
I maintainer di solito rispondono entro 2 giorni
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 25/100
- Tipo di issue
- Bug
- Chiarezza
- Da chiarire
- Stato di attività
- Attiva
- Stack tecnologico
- react, typescript
- Ambito
- frontend
Direzione di ricerca
No repository file or test is named. Start with the framework-agnostic replay pattern, then trace ProseMirror's DOMObserver.flush/readDOMChange path and the ViewTreeUpdater/NodeViewDesc reuse decision. Done requires an agreed upstream fix or API, plus a regression test showing a custom block keeps its React state after the triggering DOM or selection changes.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Upstream BlockNote issue draft: PM node-view recreation discards React state in custom blocks
Reported from a production OSS editor (Windlass) built on BlockNote 0.55.
Proposed title
Custom block React state silently resets/unmounts when ProseMirror recreates the node view (DOMObserver treats external DOM writes as content changes)
Environment
@blocknote/core/@blocknote/react/@blocknote/shadcn: 0.55.0react/react-dom: 19.3.0yjs: 13.6.33,y-prosemirror: 1.3.7- ProseMirror (resolved):
prosemirror-view1.42.6,prosemirror-state1.4.4,prosemirror-model1.25.12,prosemirror-transform1.12.2 - Radix stack under the block's chrome:
@radix-ui/react-dialog1.1.23,vaul1.1.2 - Browser: Chromium (Chrome 141-class); reproduced in dev and in jsdom-based replay tests.
Minimal repro
- Render a BlockNote doc containing a custom block whose React component holds
useState(e.g. an open/closed flag for an in-block dialog, or any form draft). - Open the dialog / focus a form field inside the custom block.
- Trigger either of the two recreation classes below (easiest: open a Radix
modal
dialog anywhere in the app soreact-remove-scrollwritesaria-hiddenonto
elements outside the dialog, including the editor's node-view DOM). - Observe: within one flush cycle (~25 ms in our traces) the custom block's React
subtree is destroyed and recreated — everyuseStateresets. An open sheet closes
itself; a half-filled form resets. The editor content is unchanged; only the block's
local UI state is lost.
Forensics (what we measured)
- The loop: PM's
DOMObserver.flush()→readDOMChangesees DOM mutations inside
the node view's DOM, fails to attribute them to a known transaction, and the view's
update cycle ends inViewTreeUpdater/NodeViewDescdeciding the container
"failsmatchesNode" and recreates the node view. React then unmounts the
custom block's subtree and mounts a fresh one — all local state gone. - Trigger class 1 — selection transactions: every selection change moves widget
decorations between DOM containers. Our block's widget DOM lands in a new parent;
if the move races the observer flush, the container diff is misread as a content
change and the block is recreated. - Trigger class 2 — Radix modal chrome: Radix (and vaul's drawer, built on it)
writesaria-hidden(viareact-remove-scroll) onto elements outside the dialog —
including BlockNote's node-view DOM inside the editor contenteditable. PM's
DOMObserver does not ignore those attribute mutations, so the containers get marked
NODE_DIRTY and the nextupdateStaterecreates the affected blocks. - Both classes share the root cause: PM cannot distinguish "DOM changed by the app
around my node views" from "content changed", and its fallback is destruction.
Any React state living inside the node view is collateral damage.
Our fixes (shipped, tested)
- Stop the modal-chrome trigger: render the block's sheets/dialogs with
modal={false}(Radix) soreact-remove-scrollnever writesaria-hiddeninto
the editor DOM. This removes one of the two recreation triggers entirely. - Survive the remaining trigger (state hoisting): move the block's UI open-state
(and any state that must survive) OUT of the block's React subtree:- a module-level store keyed by block id, read through
useSyncExternalStore; - the sheet rendered through a STABLE module-level React
createRooton a detached
host div outside the editor DOM, so a recreation of the owning block cannot
unmount it; - handler indirection ("bridge") so the stable-root-rendered sheet always calls the
current instance's callbacks after a recreation re-syncs them.
Regression tests replay the destruction (unmount + fresh mount of the block while
the sheet is open) and assert the sheet stays open, keeps its draft, and completes
its flow through the recreated instance.
- a module-level store keyed by block id, read through
Fix 2 works but is a lot of ceremony for "a checkbox that survives a repaint"; we'd
rather not keep building it per block.
Proposed upstream directions (pick one, or propose better)
- Node-view reuse stability guarantee: make
DOMObserverignore attribute-only
mutations that PM itself did not observe as content (or at leastaria-hidden/
data-*writes by known UI libraries), and makeViewTreeUpdaterprefer updating
an existing node view over recreate when only the container's position moved.
Concretely: treat container moves caused by widget decoration redraws as non-content
events. - Official external-store escape hatch: a first-class API for custom blocks to
declare state that must survive node-view recreation (e.g. auseBlockStableState
hook or a per-block "state root" the host mounts outside the node view), so apps
don't hand-roll module-level stores + stable roots + handler bridges. - Documentation escape: if neither is feasible short-term, document the hazard
(state inside custom blocks is ephemeral under recreation; modal chrome writes into
the editor DOM are a trigger) prominently, with themodal={false}recommendation
for any overlay mounted near editor content.
Reproduction pointers for maintainers
- Our replay pattern is framework-agnostic: mount a custom block with a controlled
Radix/vaul sheet inside it, open the sheet, dispatch a selection transaction or open
a second modal elsewhere, and watch the block's React subtree remount (React DevTools
or a mount counter shows the reset). - Happy to provide a standalone repro repo if useful.
- Lingua principale
- TypeScript
- Stelle
- 10.3k
- Fork
- 778
- Merge medio
- 2g 14h
- PR unite (30g)
- 27
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di TypeCellOS/BlockNote
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
TypeCellOS/BlockNote#2949 · 1 commento ·
I maintainer di solito rispondono entro 2 giorni
-
a11y
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
TypeCellOS/BlockNote#2855 ·
I maintainer di solito rispondono entro 2 giorni
-
a11y
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
TypeCellOS/BlockNote#2829 · 1 commento ·
I maintainer di solito rispondono entro 2 giorni
-
Comment thread actions: hover-only controls and unlabeled buttons in ThreadsSidebaForse già presa Una pull request collegata a questa issue è aperta o già unita. Apertaa11y
Difficoltà 2/5 1-3 ore Idoneità per principianti 72/100
TypeCellOS/BlockNote#2824 ·
I maintainer di solito rispondono entro 2 giorni
-
Collapsible list: unlabeled accessible button, decorative imageForse già presa @adarshsm l’ha presa 9 giorni fa. Apertaa11y
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
TypeCellOS/BlockNote#2811 ·
I maintainer di solito rispondono entro 2 giorni
Tutte le issue di TypeCellOS/BlockNote
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
rajbos/ai-engineering-fluency#2340 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
community documentation first-timers-only good first issue hacktoberfest help wanted low hanging fruit up-for-grabs
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 70/100
lingdojo/kana-dojo#31864 · 1 commento · 5 reazioni ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
zenstackhq/zenstack#2873 ·
I maintainer di solito rispondono entro 1 giorno
-
CLI: TUI shows onboarding when the provider's API key is only in the environment (e.g. OPENROUTER_API_KEY)Forse già presa Una pull request collegata a questa issue è aperta o già unita. ApertaCLI
Difficoltà 2/5 1-3 ore Idoneità per principianti 67/100
cline/cline#14923 · 2 commenti ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
paperclipai/paperclip#15490 ·
I maintainer di solito rispondono entro 1 giorno