This guide walks through moving a Slate 0.x editor to Slate v2. The migration is
mostly a runtime migration: create the editor with an initial value, read with
editor.read(...), write with editor.update(...), and render through the v2
React runtime.
Warning: Slate v2 is a hard API cut. Do not migrate by keeping old helpers alive behind aliases. Move one surface at a time, keep the old editor working until the v2 editor has matching behavior, and delete each old path as soon as the replacement is verified.
On This Page
- Should You Migrate This Editor?
- First Migration Slice
- Migration Map
- Step 1: Install The Packages
- Step 14: Check Import Boundaries
- Proof Before Deleting The Old Path
- Validation Checklist
Should You Migrate This Editor?
Migrate when the v2 runtime solves a real ownership problem in your editor. Do not migrate just to rename APIs.
| Migrate this surface first | Wait or defer |
|---|---|
| A representative editor that users type in, save, undo, and paste into. | A low-value editor with no active product pressure. |
| An editor that needs cleaner command ownership, document state, overlays, extra roots, or large-document proof. | An editor whose only goal is to keep old Slate 0.x behavior with the smallest possible diff. |
| A surface where you can run browser proof for selection, paste, undo, and save/load. | A surface whose correctness depends on mobile-device proof, collaboration adapters, or production pagination that your app cannot verify yet. |
Keep the old editor path alive until the v2 path proves the same user-visible behavior. If that sounds expensive, the migration is not ready.
First Migration Slice
Port the thinnest useful editor before touching every command or reusable feature:
- Create one v2 editor with the same representative document value.
- Render one
<Editable>with the existing element and leaf renderers. - Save the primary value on
change.valueChanged. - Port one toolbar or shortcut command to
editor.read(...)andeditor.update(...). - Verify typing, selection, paste, undo/redo, and save/load in the browser.
- Delete only the old code covered by that proof.
Do not start with comments, collaboration, pagination, custom mobile input, or every reusable feature. Those belong after the basic editor behaves correctly.
Before You Begin
Start with the smallest editor surface that users can type in, save, and undo. Migrate commands, custom elements, and persistence after that editor is stable.
You should have:
- one representative document value;
- the custom element and text types used by that editor;
- the commands your app calls from toolbars, shortcuts, and paste handlers;
- the tests that prove typing, selection, copy/paste, undo/redo, and save/load.
Do not start by porting every reusable feature. That is how migrations turn into archaeology.
Migration Map
| Slate 0.x | Slate v2 |
|---|---|
createEditor() plus withReact(editor) | useSlateEditor({ initialValue }) in React, or createReactEditor({ initialValue }) outside a component |
withHistory(editor) | useSlateEditor(...) for the default React editor, or history() when building an editor manually |
<Slate editor={editor} initialValue={value}> | const editor = useSlateEditor({ initialValue }); <Slate editor={editor}> |
| Slate 0.x transform helper calls | editor.update((tx) => tx.*...) |
Editor.* reads that inspect the current editor | editor.read((state) => state.*...) |
| Pure node, point, path, range, and text helpers | NodeApi, ElementApi, PointApi, PathApi, RangeApi, and TextApi from @platejs/slate |
ReactEditor.* DOM bridge calls | editor.api.dom.*, editor.api.react.*, or renderer hooks such as useElementPath() |
useSlate, useSlateStatic | useEditor() and useEditorState(...) |
useSelected, useFocused, useReadOnly | useElementSelected(...), useEditorFocused(), and useEditorReadOnly() |
onChange(value) as the primary persistence hook | onChange(value, change) and change.valueChanged, or state.value.get() for the full document |
editor-only children persistence | primary children, optional extra roots, and optional document state |
decorate for simple local ranges | <Editable decorate={...} /> |
decorate for shared, external, frequent, or source-scoped overlays | useSlateDecorationSource, useSlateRangeDecorationSource, or useSlateAnnotationStore |
Step 1: Install The Packages
For the beta package set, install the core editor, DOM helpers, React renderer, and React peer packages.
pnpm add @platejs/slate @platejs/slate-dom @platejs/slate-react react react-dompnpm add @platejs/slate @platejs/slate-dom @platejs/slate-react react react-domInstall the optional packages only when your app imports them directly.
pnpm add @platejs/slate-history @platejs/slate-hyperscript @platejs/yjspnpm add @platejs/slate-history @platejs/slate-hyperscript @platejs/yjsPackage ownership is split deliberately:
@platejs/slateowns the editor runtime, document model, transactions, state fields, and pure helper APIs.@platejs/slate-domowns DOM resolution, clipboard helpers, hotkeys, and browser environment helpers.@platejs/slate-reactowns<Slate>,<Editable>, render primitives, React hooks, DOM repair, and the React editor extension.@platejs/slate-historyowns the history extension and history API.@platejs/slate-hyperscriptowns JSX fixtures for tests.@platejs/yjsowns the Yjs adapter, awareness, provider lifecycle bridge, and remote cursor hooks. App code owns provider packages and server policy.
Step 2: Create The Editor
The v2 editor owns its initial value. <Slate> receives an editor that already
exists.
Slate 0.x:
import { useMemo } from 'react'
const App = () => {
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
return (
<Slate editor={editor} initialValue={initialValue}>
<Editable />
</Slate>
)
}import { useMemo } from 'react'
const App = () => {
const editor = useMemo(() => withHistory(withReact(createEditor())), [])
return (
<Slate editor={editor} initialValue={initialValue}>
<Editable />
</Slate>
)
}Slate v2:
import { Editable, Slate, useSlateEditor } from '@platejs/slate-react'
const App = () => {
const editor = useSlateEditor({ initialValue })
return (
<Slate editor={editor}>
<Editable />
</Slate>
)
}import { Editable, Slate, useSlateEditor } from '@platejs/slate-react'
const App = () => {
const editor = useSlateEditor({ initialValue })
return (
<Slate editor={editor}>
<Editable />
</Slate>
)
}Use createReactEditor when the editor must be created outside a component,
such as a test helper or app service.
import { createReactEditor } from '@platejs/slate-react'
const editor = createReactEditor({ initialValue })import { createReactEditor } from '@platejs/slate-react'
const editor = createReactEditor({ initialValue })Use createEditor plus extensions for lower-level runtime construction.
import { createEditor } from '@platejs/slate'
import { history } from '@platejs/slate-history'
import { react } from '@platejs/slate-react'
const editor = createEditor({
initialValue,
extensions: [react(), history()],
})import { createEditor } from '@platejs/slate'
import { history } from '@platejs/slate-history'
import { react } from '@platejs/slate-react'
const editor = createEditor({
initialValue,
extensions: [react(), history()],
})Step 3: Move Reads And Writes Into The Runtime
Slate v2 separates reads from writes. Reads happen inside editor.read(...).
Writes happen inside editor.update(...).
Slate 0.x commands often mixed read helpers and transform helpers directly against the editor object. In Slate v2, read the committed runtime state and write through a transaction.
Slate v2:
import { ElementApi } from '@platejs/slate'
const isCodeBlockActive = editor.read(state =>
Boolean(
state.nodes.find({
match: node => ElementApi.isElement(node) && node.type === 'code',
})
)
)
editor.update(tx => {
tx.nodes.set(
{ type: 'code' },
{
match: node => ElementApi.isElement(node) && tx.schema.isBlock(node),
}
)
})import { ElementApi } from '@platejs/slate'
const isCodeBlockActive = editor.read(state =>
Boolean(
state.nodes.find({
match: node => ElementApi.isElement(node) && node.type === 'code',
})
)
)
editor.update(tx => {
tx.nodes.set(
{ type: 'code' },
{
match: node => ElementApi.isElement(node) && tx.schema.isBlock(node),
}
)
})For text commands, write through tx.text.
editor.update(tx => {
tx.text.insert('Hello')
})editor.update(tx => {
tx.text.insert('Hello')
})For selection commands, write through tx.selection.
editor.update(tx => {
tx.selection.set({
anchor: { path: [0, 0], offset: 0 },
focus: { path: [0, 0], offset: 5 },
})
})editor.update(tx => {
tx.selection.set({
anchor: { path: [0, 0], offset: 0 },
focus: { path: [0, 0], offset: 5 },
})
})For pure data checks, import the helper API from @platejs/slate.
import { NodeApi, RangeApi, TextApi } from '@platejs/slate'
const plainText = NodeApi.string(element)
const isCollapsed = selection ? RangeApi.isCollapsed(selection) : false
const isText = TextApi.isText(node)import { NodeApi, RangeApi, TextApi } from '@platejs/slate'
const plainText = NodeApi.string(element)
const isCollapsed = selection ? RangeApi.isCollapsed(selection) : false
const isText = TextApi.isText(node)Step 4: Migrate Custom Elements
Renderers are still React functions. The critical rule is unchanged: spread
attributes on the top-level DOM element and render children.
const CodeElement = ({ attributes, children }) => {
return (
<pre {...attributes}>
<code>{children}</code>
</pre>
)
}
const DefaultElement = ({ attributes, children }) => {
return <p {...attributes}>{children}</p>
}
const renderElement = props => {
switch (props.element.type) {
case 'code':
return <CodeElement {...props} />
default:
return <DefaultElement {...props} />
}
}
const App = () => {
const editor = useSlateEditor({ initialValue })
return (
<Slate editor={editor}>
<Editable renderElement={renderElement} />
</Slate>
)
}const CodeElement = ({ attributes, children }) => {
return (
<pre {...attributes}>
<code>{children}</code>
</pre>
)
}
const DefaultElement = ({ attributes, children }) => {
return <p {...attributes}>{children}</p>
}
const renderElement = props => {
switch (props.element.type) {
case 'code':
return <CodeElement {...props} />
default:
return <DefaultElement {...props} />
}
}
const App = () => {
const editor = useSlateEditor({ initialValue })
return (
<Slate editor={editor}>
<Editable renderElement={renderElement} />
</Slate>
)
}Keep renderer functions stable. Define them at module scope or memoize them once.
Step 5: Migrate Leaves, Text, And Placeholders
Custom leaf and text renderers still receive Slate attributes and children. Keep the renderer small and pass Slate's props through.
const renderLeaf = ({ attributes, children, leaf }) => {
return (
<span
{...attributes}
style={{ fontWeight: leaf.bold ? 'bold' : undefined }}
>
{children}
</span>
)
}
const App = () => {
const editor = useSlateEditor({ initialValue })
return (
<Slate editor={editor}>
<Editable placeholder="Write something..." renderLeaf={renderLeaf} />
</Slate>
)
}const renderLeaf = ({ attributes, children, leaf }) => {
return (
<span
{...attributes}
style={{ fontWeight: leaf.bold ? 'bold' : undefined }}
>
{children}
</span>
)
}
const App = () => {
const editor = useSlateEditor({ initialValue })
return (
<Slate editor={editor}>
<Editable placeholder="Write something..." renderLeaf={renderLeaf} />
</Slate>
)
}Use SlateElement, SlateText, SlateLeaf, and SlatePlaceholder only when
you replace Slate's rendering primitives and still need the required DOM
attributes attached. Normal apps should start with renderElement, renderLeaf,
and placeholder.
Step 6: Migrate Event Handlers
Editable owns the default browser behavior. App handlers can run before Slate
and decide whether Slate continues.
- Return
truewhen your handler handled the event and Slate should stop. - Return
falsewhen Slate should run its default handler. - Return nothing when Slate should decide from the DOM event state.
Slate 0.x event handlers often prevented the browser event and mutated text nodes directly. In Slate v2, return a handler decision and route the write through the editor runtime.
Slate v2:
<Editable
onKeyDown={(event, { editor }) => {
if (!(event.metaKey && event.key === 'b')) return false
editor.update(tx => {
tx.marks.toggle('bold')
})
return true
}}
/><Editable
onKeyDown={(event, { editor }) => {
if (!(event.metaKey && event.key === 'b')) return false
editor.update(tx => {
tx.marks.toggle('bold')
})
return true
}}
/>Use onDOMBeforeInput for browser input types that need editor context.
<Editable
onDOMBeforeInput={(event, { editor, inputType }) => {
if (inputType !== 'formatBold') return false
editor.update(tx => {
tx.marks.toggle('bold')
})
return true
}}
/><Editable
onDOMBeforeInput={(event, { editor, inputType }) => {
if (inputType !== 'formatBold') return false
editor.update(tx => {
tx.marks.toggle('bold')
})
return true
}}
/>Step 7: Migrate React Hooks
Use editor hooks for editor-wide UI and element hooks for rendered nodes.
| Slate 0.x hook | Slate v2 hook |
|---|---|
useSlate() | useEditor() when you need the editor object |
useSlateSelector(...) | useEditorState(...) for state reads, or useEditorSelector(...) for low-level editor reads |
useSlateSelection() | useEditorSelection() |
useFocused() | useEditorFocused() |
useReadOnly() | useEditorReadOnly() |
useSelected() | useElementSelected(...) |
Toolbar state should subscribe to a derived value.
import { useEditorState } from '@platejs/slate-react'
const BoldButton = () => {
const isBold = useEditorState(state => {
return state.marks.get()?.bold === true
})
return <button aria-pressed={isBold}>Bold</button>
}import { useEditorState } from '@platejs/slate-react'
const BoldButton = () => {
const isBold = useEditorState(state => {
return state.marks.get()?.bold === true
})
return <button aria-pressed={isBold}>Bold</button>
}Rendered element UI should subscribe only when it draws selection state.
import { useElementSelected } from '@platejs/slate-react'
const ImageElement = ({ attributes, children }) => {
const selected = useElementSelected({ mode: 'collapsed' })
return (
<figure {...attributes} data-selected={selected ? '' : undefined}>
{children}
</figure>
)
}import { useElementSelected } from '@platejs/slate-react'
const ImageElement = ({ attributes, children }) => {
const selected = useElementSelected({ mode: 'collapsed' })
return (
<figure {...attributes} data-selected={selected ? '' : undefined}>
{children}
</figure>
)
}Step 8: Migrate DOM And React Editor Calls
Do not import ReactEditor as a value object for DOM bridge calls. Use the
editor API or renderer hooks.
Slate 0.x:
import { ReactEditor } from '@platejs/slate-react'
const path = ReactEditor.findPath(editor, element)
const domNode = ReactEditor.toDOMNode(editor, element)
ReactEditor.focus(editor)import { ReactEditor } from '@platejs/slate-react'
const path = ReactEditor.findPath(editor, element)
const domNode = ReactEditor.toDOMNode(editor, element)
ReactEditor.focus(editor)Slate v2:
const path = editor.api.dom.resolvePath(element)
if (!path) return
editor.api.dom.focus()const path = editor.api.dom.resolvePath(element)
if (!path) return
editor.api.dom.focus()Inside an element renderer, prefer useElementPath() when the component must
render path-derived UI.
import { useElementPath } from '@platejs/slate-react'
const BlockToolbarAnchor = () => {
const path = useElementPath()
return <span data-path={path?.join('.')} />
}import { useElementPath } from '@platejs/slate-react'
const BlockToolbarAnchor = () => {
const path = useElementPath()
return <span data-path={path?.join('.')} />
}Use editor.api.react.isComposing() for React runtime state that belongs to the
React extension.
const composing = editor.api.react.isComposing()const composing = editor.api.react.isComposing()Step 9: Migrate History
The normal React setup includes history. Use the history hook for UI controls.
import { useSlateHistory } from '@platejs/slate-react'
const HistoryButtons = () => {
const history = useSlateHistory()
return (
<>
<button disabled={!history.canUndo} onClick={history.undo}>
Undo
</button>
<button disabled={!history.canRedo} onClick={history.redo}>
Redo
</button>
</>
)
}import { useSlateHistory } from '@platejs/slate-react'
const HistoryButtons = () => {
const history = useSlateHistory()
return (
<>
<button disabled={!history.canUndo} onClick={history.undo}>
Undo
</button>
<button disabled={!history.canRedo} onClick={history.redo}>
Redo
</button>
</>
)
}For a manually created editor, install the history extension.
import { createEditor } from '@platejs/slate'
import { history } from '@platejs/slate-history'
const editor = createEditor({
initialValue,
extensions: [history()],
})import { createEditor } from '@platejs/slate'
import { history } from '@platejs/slate-history'
const editor = createEditor({
initialValue,
extensions: [history()],
})Disable default history in a React editor when a test or app surface needs that explicitly.
import { history } from '@platejs/slate-history'
import { useSlateEditor } from '@platejs/slate-react'
const App = () => {
const editor = useSlateEditor({
initialValue,
extensions: [history({ enabled: false })],
})
return (
<Slate editor={editor}>
<Editable />
</Slate>
)
}import { history } from '@platejs/slate-history'
import { useSlateEditor } from '@platejs/slate-react'
const App = () => {
const editor = useSlateEditor({
initialValue,
extensions: [history({ enabled: false })],
})
return (
<Slate editor={editor}>
<Editable />
</Slate>
)
}Step 10: Persist The Right Value
For a single-root editor, save the value passed to <Slate onChange>.
import { type SlateChange } from '@platejs/slate-react'
const handleChange = (value, change: SlateChange) => {
if (!change.valueChanged) return
saveDocument(value)
}
;<Slate editor={editor} onChange={handleChange}>
<Editable />
</Slate>import { type SlateChange } from '@platejs/slate-react'
const handleChange = (value, change: SlateChange) => {
if (!change.valueChanged) return
saveDocument(value)
}
;<Slate editor={editor} onChange={handleChange}>
<Editable />
</Slate>For a document with extra roots or document state, read the full document value.
const documentValue = editor.read(state => state.value.get())const documentValue = editor.read(state => state.value.get())The short value is still a block array.
const initialValue = [
{
type: 'paragraph',
children: [{ text: 'A line of text!' }],
},
]const initialValue = [
{
type: 'paragraph',
children: [{ text: 'A line of text!' }],
},
]Use the document value shape when the editor owns more than the primary document.
const initialValue = {
children: [
{
type: 'paragraph',
children: [{ text: 'A line of text!' }],
},
],
roots: {
header: [
{
type: 'paragraph',
children: [{ text: 'Draft' }],
},
],
},
state: {
'document.title': 'Draft',
},
}const initialValue = {
children: [
{
type: 'paragraph',
children: [{ text: 'A line of text!' }],
},
],
roots: {
header: [
{
type: 'paragraph',
children: [{ text: 'Draft' }],
},
],
},
state: {
'document.title': 'Draft',
},
}Omit root for the primary editable. Pass root only for extra roots.
<Slate editor={editor}>
<Editable aria-label="Body" />
<Editable aria-label="Header" root="header" />
</Slate><Slate editor={editor}>
<Editable aria-label="Body" />
<Editable aria-label="Header" root="header" />
</Slate>Step 11: Migrate Document State
Use defineStateField for document-level state that should travel with the
document, such as title, layout settings, review mode, spellcheck metadata, or
collaboration-owned state.
import { defineStateField } from '@platejs/slate'
import {
Editable,
Slate,
useSetStateField,
useSlateEditor,
useStateFieldValue,
} from '@platejs/slate-react'
const documentTitle = defineStateField<string>({
key: 'document.title',
initial: () => 'Untitled',
persist: true,
})
const TitleInput = () => {
const title = useStateFieldValue(documentTitle)
const setTitle = useSetStateField(documentTitle)
return (
<input value={title} onChange={event => setTitle(event.target.value)} />
)
}
const App = () => {
const editor = useSlateEditor({
extensions: [documentTitle],
initialValue,
})
return (
<Slate editor={editor}>
<TitleInput />
<Editable />
</Slate>
)
}import { defineStateField } from '@platejs/slate'
import {
Editable,
Slate,
useSetStateField,
useSlateEditor,
useStateFieldValue,
} from '@platejs/slate-react'
const documentTitle = defineStateField<string>({
key: 'document.title',
initial: () => 'Untitled',
persist: true,
})
const TitleInput = () => {
const title = useStateFieldValue(documentTitle)
const setTitle = useSetStateField(documentTitle)
return (
<input value={title} onChange={event => setTitle(event.target.value)} />
)
}
const App = () => {
const editor = useSlateEditor({
extensions: [documentTitle],
initialValue,
})
return (
<Slate editor={editor}>
<TitleInput />
<Editable />
</Slate>
)
}Read fields inside editor.read(...) and write fields inside
editor.update(...).
const title = editor.read(state => state.getField(documentTitle))
editor.update(tx => {
tx.setField(documentTitle, 'Launch Brief')
})const title = editor.read(state => state.getField(documentTitle))
editor.update(tx => {
tx.setField(documentTitle, 'Launch Brief')
})Step 12: Migrate Extra Roots
Use extra roots when one editor owns multiple editable regions that share history, schema, commands, and document state.
const editor = useSlateEditor({
initialValue: {
children: [{ type: 'paragraph', children: [{ text: 'Body' }] }],
roots: {
header: [{ type: 'paragraph', children: [{ text: 'Header' }] }],
footer: [{ type: 'paragraph', children: [{ text: 'Footer' }] }],
},
},
})const editor = useSlateEditor({
initialValue: {
children: [{ type: 'paragraph', children: [{ text: 'Body' }] }],
roots: {
header: [{ type: 'paragraph', children: [{ text: 'Header' }] }],
footer: [{ type: 'paragraph', children: [{ text: 'Footer' }] }],
},
},
})Render the primary document without a root prop. Render extra roots by key.
<Slate editor={editor}>
<Editable aria-label="Header" root="header" />
<Editable aria-label="Body" />
<Editable aria-label="Footer" root="footer" />
</Slate><Slate editor={editor}>
<Editable aria-label="Header" root="header" />
<Editable aria-label="Body" />
<Editable aria-label="Footer" root="footer" />
</Slate>Use root-aware hooks for chrome and commands outside editable content.
import { useSlateRootChrome, useSlateRootEditor } from '@platejs/slate-react'
const HeaderChrome = ({ children }) => {
const chrome = useSlateRootChrome('header')
const headerEditor = useSlateRootEditor('header')
return (
<section {...chrome.props}>
<button onClick={() => headerEditor.update(tx => tx.text.insert('!'))}>
Add
</button>
{children}
</section>
)
}import { useSlateRootChrome, useSlateRootEditor } from '@platejs/slate-react'
const HeaderChrome = ({ children }) => {
const chrome = useSlateRootChrome('header')
const headerEditor = useSlateRootEditor('header')
return (
<section {...chrome.props}>
<button onClick={() => headerEditor.update(tx => tx.text.insert('!'))}>
Add
</button>
{children}
</section>
)
}Step 13: Migrate Hyperscript Tests
Keep hyperscript in tests and fixtures. Runtime editor code should use normal Slate node values.
/** @jsx jsx */
import { jsx } from '@platejs/slate-hyperscript'
const editor = (
<editor>
<element type="paragraph">
alpha
<cursor />
</element>
</editor>
)/** @jsx jsx */
import { jsx } from '@platejs/slate-hyperscript'
const editor = (
<editor>
<element type="paragraph">
alpha
<cursor />
</element>
</editor>
)Use createHyperscript when your test suite needs domain-specific tags.
import { createHyperscript } from '@platejs/slate-hyperscript'
const h = createHyperscript({
elements: {
paragraph: { type: 'paragraph' },
},
})
const paragraph = h('paragraph', {}, 'hello')import { createHyperscript } from '@platejs/slate-hyperscript'
const h = createHyperscript({
elements: {
paragraph: { type: 'paragraph' },
},
})
const paragraph = h('paragraph', {}, 'hello')Step 14: Check Import Boundaries
Use public package imports. Do not deep import from package internals.
import {
createEditor,
defineEditorExtension,
defineStateField,
ElementApi,
NodeApi,
RangeApi,
TextApi,
} from '@platejs/slate'
import { DOMCoverage, dom, Hotkeys, isHotkey } from '@platejs/slate-dom'
import {
Editable,
Slate,
createReactEditor,
useEditor,
useEditorState,
useSlateEditor,
} from '@platejs/slate-react'
import { history } from '@platejs/slate-history'
import { createHyperscript, jsx } from '@platejs/slate-hyperscript'import {
createEditor,
defineEditorExtension,
defineStateField,
ElementApi,
NodeApi,
RangeApi,
TextApi,
} from '@platejs/slate'
import { DOMCoverage, dom, Hotkeys, isHotkey } from '@platejs/slate-dom'
import {
Editable,
Slate,
createReactEditor,
useEditor,
useEditorState,
useSlateEditor,
} from '@platejs/slate-react'
import { history } from '@platejs/slate-history'
import { createHyperscript, jsx } from '@platejs/slate-hyperscript'The ReactEditor export in @platejs/slate-react is a type. Do not build runtime code
around ReactEditor.* static calls.
Proof Before Deleting The Old Path
Delete old Slate code only after the v2 path proves the behavior users depend on. Model assertions are useful, but they do not prove browser selection, clipboard, focus, or DOM repair.
| Behavior | Minimum proof |
|---|---|
| Typing and deletion | Type text, Backspace/Delete, Enter, and follow-up typing after command changes. |
| Selection | Check model selection and native selection for arrows, Shift+arrows, drag, double-click, and blank-space clicks when the surface supports them. |
| Clipboard and paste | Verify copy, cut, paste, and app-specific paste normalization through browser events. |
| Undo/redo | Prove command grouping, text input, paste, and structural edits undo and redo in the expected order. |
| Custom rendering | Verify custom elements, leaves, voids, placeholders, hidden content, and DOM coverage paths used by the editor. |
| Persistence | Save and reload the exact value shape the editor owns: primary children, plus extra roots or document state when used. |
For selection-sensitive editors, run browser proof before deleting the old path. For app-only commands, focused unit tests can be enough after one browser path has proven the runtime integration.
Validation Checklist
Run the migration in behavior order:
- Render the smallest editor with
useSlateEditor,<Slate>, and<Editable>. - Type normal text and save only when
change.valueChangedis true. - Port one command to
editor.read(...)pluseditor.update(...). - Port custom element and leaf renderers.
- Verify keyboard shortcuts, paste, copy, drag/drop, undo/redo, and selection.
- Persist
state.value.get()if the editor uses extra roots or document state. - Move tests to current public imports.
- Delete the old editor wrapper after the v2 editor owns the same behavior.
The migration is complete when the editor behaves the same from the user's point of view and the codebase no longer depends on old helper aliases.
API Reference Links
On This Page
On This PageShould You Migrate This Editor?First Migration SliceBefore You BeginMigration MapStep 1: Install The PackagesStep 2: Create The EditorStep 3: Move Reads And Writes Into The RuntimeStep 4: Migrate Custom ElementsStep 5: Migrate Leaves, Text, And PlaceholdersStep 6: Migrate Event HandlersStep 7: Migrate React HooksStep 8: Migrate DOM And React Editor CallsStep 9: Migrate HistoryStep 10: Persist The Right ValueStep 11: Migrate Document StateStep 12: Migrate Extra RootsStep 13: Migrate Hyperscript TestsStep 14: Check Import BoundariesProof Before Deleting The Old PathValidation ChecklistAPI Reference Links