The Editor object is the runtime for one Slate document. It owns the document value, selection, operations, schema behavior, extensions, and subscriptions.
Most application code touches the editor in three ways:
- read committed state with
editor.read(...) - write through
editor.update((tx) => ...) - install reusable behavior with
editor.extend(...)
Reading State
Use editor.read(...) when you need a consistent view of editor state. Slate passes a grouped state object into the callback.
const info = editor.read((state) => {
return {
selection: state.selection.get(),
text: state.text.string([]),
value: state.value.get(),
}
})const info = editor.read((state) => {
return {
selection: state.selection.get(),
text: state.text.string([]),
value: state.value.get(),
}
})The callback is read-only. Starting a write from inside a read is rejected because it would mix two different editor snapshots.
Writing State
Use editor.update(...) when you need to change the document or selection. Slate passes a transaction object into the callback.
editor.update((tx) => {
tx.text.insert('!')
tx.nodes.set({ type: 'heading-one' }, { at: [0] })
tx.selection.set(tx.points.end([]))
})editor.update((tx) => {
tx.text.insert('!')
tx.nodes.set({ type: 'heading-one' }, { at: [0] })
tx.selection.set(tx.points.end([]))
})All writes in the callback become one commit. That gives history, operation replay, sync adapters, and React rendering one consistent change to observe.
Snapshots And Commits
Slate exposes committed state through subscriptions. Most application reads
should use narrow state groups like state.value, state.selection, and
extension-owned state.
const unsubscribe = editor.subscribe((_snapshot, change) => {
if (change?.childrenChanged || change?.dirtyStateKeys.length) {
const documentValue = editor.read((state) => state.value.get())
saveDocument(documentValue)
}
})const unsubscribe = editor.subscribe((_snapshot, change) => {
if (change?.childrenChanged || change?.dirtyStateKeys.length) {
const documentValue = editor.read((state) => state.value.get())
saveDocument(documentValue)
}
})Use subscriptions for app services that need to observe commits. Use React hooks from @platejs/slate-react for UI that renders editor state.
Full snapshots are runtime observer data. Use them for debug, replay, and test tooling that intentionally needs the whole document, selection, marks, operation index, and runtime id index.
const snapshot = editor.read((state) => state.runtime.snapshot())const snapshot = editor.read((state) => state.runtime.snapshot())Local Provenance
Use update tag values for cheap lifecycle labels and metadata.origin for
typed local policy. A paste handler, import tool, or command palette action can
label the commit without adding provenance fields to document nodes.
editor.update(
tx => {
tx.text.insert(importedText)
},
{
tag: ['paste', 'import'],
metadata: {
origin: { kind: 'clipboard', source: 'html' },
},
}
)editor.update(
tx => {
tx.text.insert(importedText)
},
{
tag: ['paste', 'import'],
metadata: {
origin: { kind: 'clipboard', source: 'html' },
},
}
)Use persist: false state fields for local provenance UI that should follow the
runtime but stay out of saved document JSON. Runtime ids are useful for local
projection and debug links; semantic product ids belong in your own model when
they need to persist.
Preserving Ranges
Use bookmarks when you need a local range to survive document edits.
const bookmark = editor.read((state) => {
const selection = state.selection.get()
return selection
? state.ranges.bookmark(selection, { affinity: 'inward' })
: null
})
editor.update((tx) => {
tx.nodes.unwrap()
const selection = bookmark?.unref()
if (selection) {
tx.selection.set(selection)
}
})const bookmark = editor.read((state) => {
const selection = state.selection.get()
return selection
? state.ranges.bookmark(selection, { affinity: 'inward' })
: null
})
editor.update((tx) => {
tx.nodes.unwrap()
const selection = bookmark?.unref()
if (selection) {
tx.selection.set(selection)
}
})Bookmarks are local runtime anchors. Store shared document state as document values, operations, and commits. Use Document State for values that need to persist with the document.
Extending The Editor
Extensions package reusable behavior without mutating random fields onto the
editor object. They can register read namespaces with state, write namespaces
with tx, schema specs, commit listeners, operation middleware, normalizer
entries, and optional runtime registration.
Here's a small extension that adds a table namespace:
import { createEditor, defineEditorExtension } from '@platejs/slate'
const tables = defineEditorExtension({
name: 'tables',
state: {
table(state) {
return {
rowCount() {
return state.nodes.children().length
},
}
},
},
tx: {
table(tx) {
return {
insertRow(text = 'row') {
tx.nodes.insert(
{
type: 'paragraph',
children: [{ text }],
},
{ at: [tx.nodes.children().length] }
)
},
}
},
},
})
const editor = createEditor()
editor.extend(tables)import { createEditor, defineEditorExtension } from '@platejs/slate'
const tables = defineEditorExtension({
name: 'tables',
state: {
table(state) {
return {
rowCount() {
return state.nodes.children().length
},
}
},
},
tx: {
table(tx) {
return {
insertRow(text = 'row') {
tx.nodes.insert(
{
type: 'paragraph',
children: [{ text }],
},
{ at: [tx.nodes.children().length] }
)
},
}
},
},
})
const editor = createEditor()
editor.extend(tables)The extension adds helpers to the read and write callbacks, not to the editor object itself.
const rows = editor.read((state) => state.table.rowCount())
editor.update((tx) => {
tx.table.insertRow()
})const rows = editor.read((state) => state.table.rowCount())
editor.update((tx) => {
tx.table.insertRow()
})This keeps extension behavior scoped to the runtime phase where it is valid.
Schema Behavior
Schema checks live on the read and transaction views. Use element specs and extension-owned schema policy to decide how Slate treats your node types.
For example, image elements can be treated as block voids, and mention elements can be treated as inline markable voids. The React renderer uses those schema facts to render the correct DOM shell.
Query Groups
The read callback exposes grouped helpers for common reads and queries.
const point = editor.read(state => state.points.start([0, 0]))
const text = editor.read(state => state.text.string(range))
for (const [node, path] of editor.read(state =>
state.nodes.entries({ at: range })
)) {
// ...
}const point = editor.read(state => state.points.start([0, 0]))
const text = editor.read(state => state.text.string(range))
for (const [node, path] of editor.read(state =>
state.nodes.entries({ at: range })
)) {
// ...
}These helpers are useful inside commands, renderers, and app services. Keep document writes inside editor.update(...).