Back to Plate

Slate React multi-root Editable DX needs package-owned root views

docs/solutions/developer-experience/2026-05-23-slate-react-multi-root-editable-dx-needs-package-owned-root-views.md

53.0.64.5 KB
Original Source

Slate React multi-root Editable DX needs package-owned root views

Problem

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.

Symptoms

  • site/examples/ts/multi-root-document.tsx imported SlateRuntime, createEditorView, useSlateRuntimeState, and useSlateViewState.
  • The example kept activeRoot in local React state and used flushSync before focusing root surfaces.
  • Title and history flows manually preserved or restored focus with window.getSelection(), document.createRange(), and selectionchange.
  • After the package DX moved into hooks, ReturnType<typeof useSlateRootEditor> in helper signatures dropped extension-specific state.history and tx.history types during bun typecheck:site.

What Didn't Work

  • Keeping <SlateRuntime><Slate root> as the canonical example shape. That is a useful substrate, but it makes app authors own runtime/view wiring.
  • Fixing the example by hiding the old code in a product wrapper. Raw Slate should expose primitives: one provider, root-bound editables, and root hooks.
  • Typing helpers with ReturnType<typeof useSlateRootEditor>. Generic hook return extraction widened the extension tuple enough for site typecheck to lose history fields.

Solution

Make the normal API one editor provider with root-bound editables:

tsx
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:

tsx
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:

tsx
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()
  })
}

Why This Works

<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.

Prevention

  • Canonical multi-root examples should contain one <Slate editor> and many <Editable root> surfaces.
  • App examples should not call createEditorView, SlateRuntime, window.getSelection, document.createRange, or dispatch selectionchange.
  • Keep source-cleanliness greps beside browser proof for public examples.
  • When a generic hook returns an extension-derived editor, do not use ReturnType<typeof hook> for exported/helper signatures if extension fields matter.
  • Browser proof must still cover focus, native selection, history, and root-local copy/paste.