docs/solutions/developer-experience/2026-05-23-slate-react-multi-root-editable-dx-needs-package-owned-root-views.md
The multi-root document example taught the runtime substrate as the normal app
API. Users had to wire SlateRuntime, create root views manually, track the
active root in React state, and repair DOM selection from app code.
site/examples/ts/multi-root-document.tsx imported SlateRuntime,
createEditorView, useSlateRuntimeState, and useSlateViewState.activeRoot in local React state and used flushSync before
focusing root surfaces.window.getSelection(), document.createRange(), and selectionchange.ReturnType<typeof useSlateRootEditor>
in helper signatures dropped extension-specific state.history and
tx.history types during bun typecheck:site.<SlateRuntime><Slate root> as the canonical example shape. That is a
useful substrate, but it makes app authors own runtime/view wiring.ReturnType<typeof useSlateRootEditor>. Generic hook
return extraction widened the extension tuple enough for site typecheck to
lose history fields.Make the normal API one editor provider with root-bound editables:
const editor = useSlateEditor({
extensions: [documentTitle],
initialValue: {
roots: {
footer: [{ type: 'paragraph', children: [{ text: 'Prepared' }] }],
header: [{ type: 'paragraph', children: [{ text: 'Confidential' }] }],
main: [{ type: 'paragraph', children: [{ text: 'Body' }] }],
},
},
})
return (
<Slate editor={editor}>
<Editable root="header" aria-label="Header editor" />
<Editable aria-label="Body editor" />
<Editable root="footer" aria-label="Footer editor" />
</Slate>
)
Keep SlateRuntime, <Slate root>, createEditorView,
useSlateRuntimeState, and useSlateViewState available for advanced hosts,
but do not teach them in the canonical app example.
Add root-named public hooks and let the package create view editors internally:
const activeRoot = useSlateActiveRoot()
const rootEditor = useSlateRootEditor(activeRoot)
const headerText = useSlateRootState('header', rootText)
rootEditor.update((tx) => {
tx.history.undo()
})
For helper signatures outside the hook call site, use the exported public editor
type instead of ReturnType over a generic hook:
import { type ReactEditor } from 'slate-react'
const updateHistory = (
editor: Pick<ReactEditor, 'update'>,
direction: 'redo' | 'undo'
) => {
editor.update((tx) => {
direction === 'undo' ? tx.history.undo() : tx.history.redo()
})
}
<Slate editor> already owns the editor runtime. Letting Editable root create
and register its root view keeps the app on normal Slate composition while the
package owns active-root selection, root-local DOM sync, and history execution.
The public ReactEditor type carries the default React and history extensions.
Using it in helper signatures avoids generic ReturnType widening and keeps
history state/transaction fields visible to TypeScript.
<Slate editor> and many
<Editable root> surfaces.createEditorView, SlateRuntime,
window.getSelection, document.createRange, or dispatch
selectionchange.ReturnType<typeof hook> for exported/helper signatures if extension fields
matter.