The Editor object is the runtime for one Plite 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(...)oreditor.read.<group>.<method>() - write through
editor.update(...)oreditor.update.<group>.<method>() - install reusable behavior with
editor.extend(...)
Reading State
Use direct read methods for one-shot reads:
const selection = editor.read.selection();
const text = editor.read.text.string([]);
const value = editor.read.value();const selection = editor.read.selection();
const text = editor.read.text.string([]);
const value = editor.read.value();Use the callback form when several reads should share one consistent state
view. Plite passes a grouped state object into the callback.
const info = editor.read((state) => {
return {
selection: state.selection(),
text: state.text.string([]),
value: state.value(),
};
});const info = editor.read((state) => {
return {
selection: state.selection(),
text: state.text.string([]),
value: state.value(),
};
});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 direct update methods for one-shot writes:
editor.update.text.insert("!");
editor.update.nodes.set({ type: "heading-one" }, { at: [0] });
editor.update.selection.set(editor.read.points.get([]));
editor.update({ history: "skip" }).value.replace({
children: [{ type: "paragraph", children: [{ text: "Reset" }] }],
selection: "end",
});editor.update.text.insert("!");
editor.update.nodes.set({ type: "heading-one" }, { at: [0] });
editor.update.selection.set(editor.read.points.get([]));
editor.update({ history: "skip" }).value.replace({
children: [{ type: "paragraph", children: [{ text: "Reset" }] }],
selection: "end",
});Use the callback form when one command groups related writes into a single commit. Plite 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.
Pass a policy first when the whole update needs history or lifecycle tags:
editor.update({ history: "new-batch", tags: "paste" }, (tx) => {
tx.fragment.insert(fragment);
tx.selection.collapse({ edge: "end" });
});editor.update({ history: "new-batch", tags: "paste" }, (tx) => {
tx.fragment.insert(fragment);
tx.selection.collapse({ edge: "end" });
});Update callbacks are synchronous. Use the active tx for every nested command;
starting another public editor.update(...) inside the callback is rejected and
rolls back the outer update.
Snapshots And Commits
Plite 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.value();
saveDocument(documentValue);
}
});const unsubscribe = editor.subscribe((_snapshot, change) => {
if (change?.childrenChanged || change?.dirtyStateKeys.length) {
const documentValue = editor.read.value();
saveDocument(documentValue);
}
});Use subscriptions for app services that need to observe commits. Use React hooks from @platejs/plite-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());Update Policies
EditorUpdatePolicy has two public fields: history and tags. History modes
are "merge", "new-batch", and "skip"; TypeScript exposes them only when
the editor has a History transaction group. Tags are ordered lifecycle labels
recorded on the commit.
editor.update({ tags: ["paste", "import"] }, (tx) => {
tx.text.insert(importedText);
});editor.update({ tags: ["paste", "import"] }, (tx) => {
tx.text.insert(importedText);
});Policy tags are applied in order, then the semantic history mode is applied.
Only the last history intent survives. Inside an atomic update,
tx.tags.add(tag) can replace that intent or add another label, and
tx.tags.has(tag) inspects the current final set.
Framework and feature packages own presets for their behavior. For example,
PliteReactUpdatePolicy.preserveSelection, YjsUpdatePolicy.remote, and
SuggestionUpdatePolicy.skip add the exact tags their runtimes consume. Plite
core does not know those product policies.
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();
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();
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 meta as document values, operations, and commits. Use Document Meta 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/plite";
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/plite";
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 grouped reads, atomic updates, and direct update methods.
const rows = editor.read((state) => state.table.rowCount());
editor.update.table.insertRow();
editor.update((tx) => {
tx.table.insertRow();
});const rows = editor.read((state) => state.table.rowCount());
editor.update.table.insertRow();
editor.update((tx) => {
tx.table.insertRow();
});Extension authors can mark a method with txOnly(...) when it only makes sense
inside an active transaction. Those methods stay on tx and are omitted from
the direct update surface.
Schema Behavior
Schema checks live on the read and transaction views. Use element specs and extension-owned schema policy to decide how Plite 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
Direct read methods cover common one-shot queries. The read callback exposes the grouped helpers when code needs a custom query or an extension-owned namespace.
const point = editor.read.points.start([0, 0]);
const text = editor.read.text.string(range);
for (const [node, path] of editor.read((state) =>
state.nodes.entries({ at: range })
)) {
// ...
}const point = editor.read.points.start([0, 0]);
const text = editor.read.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(...).