docs/solutions/ui-bugs/2026-04-23-slate-react-unsynced-dom-text-ops-must-force-react-fallback.md
The direct DOM text lane skipped composition text updates correctly, but React still treated the text operation as handled by the fast path. The visible DOM stayed stale while the Slate model moved forward.
Editable falls back to React updates while composing showed alpha in the
DOM after Transforms.insertText(editor, '!').didSyncTextPathToDOM(editor, [0, 0]) correctly stayed false.createEditor() because withReact owns the onChange bridge.insert_text / remove_text operation as a React-skip is too
broad. That only works when direct DOM sync actually mutates the text node.Editable event path depends on the React/DOM plugin wrapping onChange.collapseToEnd() during composition repair without a DOM range turns
an empty browser selection into a runtime exception.Make syncTextOperationsToDOM(...) report whether text operations were actually
synced. Slate can then force selector updates only when the capability
declines:
const textSync = syncTextOperationsToDOM(editor, nextOperations)
const hasUnsyncedTextOperation =
textSync.textOperationCount > textSync.syncedTextOperationCount
handleSelectorChange(hasUnsyncedTextOperation ? undefined : nextOperations)
Keep React event fixtures honest:
const editor = withReact(createEditor())
Fail closed during composing selection repair:
if (domSelection.rangeCount > 0) {
domSelection.collapseToEnd()
} else {
domSelection.setBaseAndExtent(
newDomRange.endContainer,
newDomRange.endOffset,
newDomRange.endContainer,
newDomRange.endOffset
)
}
Direct DOM sync is a capability, not a blanket render-skip rule. Composition, custom rendering, projections, placeholders, and other opt-out cases still need React to render the changed text. Counting attempted text ops versus synced text ops keeps the fast path fast while preserving fallback correctness.
withReact(createEditor()) is the real React event fixture because it installs
the DOM/onChange bridge used by Editable.
withReact(createEditor()).