docs/solutions/developer-experience/2026-04-09-slate-v2-reacteditor-should-ride-the-mounted-bridge-and-keep-base-components-standalone.md
slate-react had recovered a lot of core editor surface, but the React package
still dropped some of the obvious extension and ergonomics seams people expect:
element hooks, default aliases, withReact, and ReactEditor. The stale docs
made it worse by describing the old plugin-era contract as if it still existed.
useElement, useElementIf, useSelected, withReact, and ReactEditor
were missing from the public barreldocs/libraries/slate-react/ described methods the current runtime
did not shipSlateElement naively caused standalone
component tests to throw because they were no longer usable outside <Slate>slate-dom plugin stack would have been a fake
victory and a lot of unnecessary codeSlateElement through useSlateStatic() broke presentational
usage outside editor contextRecover the current React compatibility seam over the mounted bridge instead of resurrecting the old plugin stack wholesale.
Key moves:
packages/slate-react/src/context.tsxuseElement, useElementIf, and useSelectedslate-react barrel:
DefaultElement, DefaultLeaf, DefaultText, DefaultPlaceholderwithReact as a compatibility constructor that records the clipboard
format key without mutating the editor instanceReactEditor helper namespace over the mounted bridge:
focus/readOnly/composing state, path/key lookup, DOM translation, and
clipboard helpersslate-dom only enough to back that seam honestly:
getRoot, hasDOMNode, toDOMNode, toSlateNode, toSlateRange,
split clipboard insertion, DOM target checks, and event-range resolutionuseSlateNodeRef optional so base presentational components still render
outside <Slate>Representative shape:
export const withReact = <T extends SlateEditor>(
editor: T,
clipboardFormatKey = 'x-slate-fragment'
): T & ReactEditor => {
setEditorClipboardFormatKey(editor, clipboardFormatKey)
return editor as T & ReactEditor
}
export const useSelected = () => {
const editor = useSlateStatic()
const path = useContext(ElementPathContext)
const selection = useSlateSelection()
if (!path || !selection || !Editor.hasPath(editor, path)) {
return false
}
return rangesOverlap(Editor.range(editor, path), selection)
}
The real seam in Slate v2 is the mounted DOM bridge plus the current immutable
snapshot model, not the old wrapper/plugin stack. Rebuilding ReactEditor over
that bridge keeps the API useful without lying about architecture that no
longer exists.
The follow-on detail matters too: DOM event helpers should resolve from the DOM target path, not by round-tripping through Slate node identity. For void targets in particular, the mounted wrapper is the stable source of truth. The same principle applies to clipboard handling: split fragment-vs-text insertion on the bridge, then let the generic helper compose them.
The provider seam follows the same rule. Slate callback classification should
diff snapshots, not stare at raw operations and hope the categories line up.
replace() is the obvious trap there: it can change children without fitting a
naive "non-selection op" heuristic.
Focused and read-only hook state follows the same rule too: if the docs describe
useFocused() and useReadOnly() as editor state, that state belongs at the
provider seam, not only inside <Editable> descendants.
The same pressure applies to rendering hooks. If the docs still describe
renderText, custom placeholder hosts, and leafPosition, either the current
text seam needs to carry them or the docs need to stop promising them.
The same rule applies to hook ordering inside render components. If a node can switch between text and element shapes at the same path, any hooks shared by both paths must run before the branch point. Otherwise the bug hides until a real browser flow flips that shape in place.
Making node binding optional is the other half of the fix. Base view components
like SlateElement need to support both mounted-editor usage and standalone
presentational usage. If those components hard-require editor context, the
recovered API instantly regresses the package's own runtime/component tests.
leafPosition, only claim it
where the current split-text seam can actually compute and prove it.yarn workspace slate-react run test
yarn test:custom
yarn lint:typescript