Plate keeps Plite's document model and moves editor setup, rendering, events, and command wiring into plugins. Migrate the editor shell first, then move custom rendering and behavior into plugins.
Use feature packages only for the nodes, marks, or behavior you add to the editor. Plate UI users should start with Plate UI instead of rebuilding every component by hand.
| Plite surface |
|---|
| Plate surface |
|---|
createEditor() plus withReact() | usePlateEditor({ ... }) in React components, or createPlateEditor({ ... }) in factories and tests. |
<Plite> plus <Editable> | <Plate> plus <PlateContent>. |
renderElement / renderLeaf switch statements | Plugin components through .configure({ component }). |
withX(editor) plugin functions | Constructor api, read, selectors, update, native Plite fields, and codec declarations built by defineCodecs. |
Top-level event handlers on Editable | Plugin on or shortcuts. |
Transforms.* imports | editor.update((tx) => tx.*). |
Editor.* imports | editor.read((state) => state.*), or a plugin-owned editor.api.* service. |
Move the editor value into the editor creation call and render the editable with PlateContent.
'use client';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
const initialValue = [
{
children: [{ text: 'Hello Plate.' }],
type: 'paragraph',
},
];
export function Editor() {
const editor = usePlateEditor({
initialValue,
});
return (
<Plate editor={editor}>
<PlateContent className="p-4" />
</Plate>
);
}'use client';
import { Plate, PlateContent, usePlateEditor } from 'platejs/react';
const initialValue = [
{
children: [{ text: 'Hello Plate.' }],
type: 'paragraph',
},
];
export function Editor() {
const editor = usePlateEditor({
initialValue,
});
return (
<Plate editor={editor}>
<PlateContent className="p-4" />
</Plate>
);
Use createPlateEditor({ ... }) when the editor is created outside React memoization.
import { createPlateEditor } from 'platejs/react';
export const editor = createPlateEditor({
initialValue: [
{
children: [{ text: 'Draft' }],
type: 'paragraph',
},
],
});import { createPlateEditor } from 'platejs/react';
export const editor = createPlateEditor({
initialValue: [
{
children: [{ text: 'Draft' }],
type: 'paragraph',
},
],
});Replace renderElement branches with node plugins. Use .configure({ component }) when the only change is the React component.
import {
ParagraphPlugin,
PlateElement,
type PlateElementProps,
} from 'platejs/react';
export function ParagraphElement({
children,
...props
}: PlateElementProps<typeof ParagraphPlugin>) {
return (
<PlateElement className="m-0 px-0 py-1" {...props}>
{children}
</PlateElement>
);
}
export const AppParagraphPlugin = ParagraphPlugin.configure({ component: ParagraphElement });import {
ParagraphPlugin,
PlateElement,
type PlateElementProps,
} from 'platejs/react';
export function ParagraphElement({
children,
...props
}: PlateElementProps<typeof ParagraphPlugin>) {
return (
<PlateElement className="m-0 px-0 py-1" {...props}>
{children}
</PlateElement>
);
}
export const AppParagraphPlugin = ParagraphPlugin.configure({ component: ParagraphElement });If an existing document uses a different persisted type, remap that element in the closed editor schema. Plugin configuration cannot rewrite schema identity.
import { schema } from 'platejs';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [AppParagraphPlugin],
schema: {
overrides: [
schema.override(AppParagraphPlugin, {
element: { type: 'p' },
}),
],
},
});import { schema } from 'platejs';
import { createPlateEditor } from 'platejs/react';
const editor = createPlateEditor({
plugins: [AppParagraphPlugin],
schema: {
overrides: [
schema.override(AppParagraphPlugin, {
element: { type: 'p' },
}),
],
},
});Put a reusable document command under the plugin's update contribution. The
command is available inside editor.update(...) under the plugin name.
import { definePlatePlugin } from 'platejs/react';
export const SignaturePlugin = definePlatePlugin('signature', {
update: ({ tx }) => ({
insert() {
tx.text.insert(' - Plate');
},
}),
});import { definePlatePlugin } from 'platejs/react';
export const SignaturePlugin = definePlatePlugin('signature', {
update: ({ tx }) => ({
insert() {
tx.text.insert(' - Plate');
},
}),
});Call it from a shortcut, toolbar, menu item, or test:
editor.update((tx) => {
tx.signature.insert();
});editor.update((tx) => {
tx.signature.insert();
});Element plugins already receive schema-inferred insert, set, and remove
updates. Do not wrap those generic operations in a feature method.
import { schema } from 'platejs';
import { definePlatePlugin } from 'platejs/react';
export const CalloutPlugin = definePlatePlugin('callout', {
schema: {
element: schema.element.textBlock(),
},
});import { schema } from 'platejs';
import { definePlatePlugin } from 'platejs/react';
export const CalloutPlugin = definePlatePlugin('callout', {
schema: {
element: schema.element.textBlock(),
},
});editor.plugin(CalloutPlugin).update.insert();editor.plugin(CalloutPlugin).update.insert();Add a custom update only when it performs behavior beyond those generic node operations. Keep that method flat and task-shaped; the portal already owns the plugin noun.
Use api for plugin-scoped immutable services, read for snapshot-local
queries, and the root native Plite fields for commands, corrections, read
middleware, lifecycle, activation, validation, and other substrate behavior.
Combine independent fields in the constructor. Use .extend() only for an
imported/prebuilt declaration, a shared factory the constructor cannot access,
or an earlier-stage type dependency.
Move editor events into the plugin that owns the behavior.
import { definePlatePlugin } from 'platejs/react';
export const TabPlugin = definePlatePlugin('tab', {
on: {
keyDown: ({ event }) => {
if (event.key !== 'Tab') return false;
event.preventDefault();
return true;
},
},
});import { definePlatePlugin } from 'platejs/react';
export const TabPlugin = definePlatePlugin('tab', {
on: {
keyDown: ({ event }) => {
if (event.key !== 'Tab') return false;
event.preventDefault();
return true;
},
},
});Use shortcuts when the key combination should call a plugin API, transform, or explicit handler.
import { definePlatePlugin } from 'platejs/react';
export const SavePlugin = definePlatePlugin('save', {
shortcuts: {
draft: {
keys: 'mod+s',
handler: ({ event }) => {
event.preventDefault();
return true;
},
},
},
});import { definePlatePlugin } from 'platejs/react';
export const SavePlugin = definePlatePlugin('save', {
shortcuts: {
draft: {
keys: 'mod+s',
handler: ({ event }) => {
event.preventDefault();
return true;
},
},
},
});Plate keeps reads and writes separate. Read editor state with editor.read(...).
Mutate the document with editor.update(...). Use editor.api.* for plugin
services and host/runtime APIs, not document mutations.
const text = editor.read((state) => state.text.string([]));
editor.update((tx) => {
tx.marks.toggle('bold');
tx.text.insert('Hello');
});
editor.api.debug.log(text);const text = editor.read((state) => state.text.string([]));
editor.update((tx) => {
tx.marks.toggle('bold');
tx.text.insert('Hello');
});
editor.api.debug.log(text);Use createBaseEditor from platejs for non-React importers, serializers, transforms, and tests.
import { createBaseEditor } from 'platejs';
export const editor = createBaseEditor({
initialValue: [
{
children: [{ text: 'Headless document.' }],
type: 'paragraph',
},
],
});import { createBaseEditor } from 'platejs';
export const editor = createBaseEditor({
initialValue: [
{
children: [{ text: 'Headless document.' }],
type: 'paragraph',
},
],
});renderElement and renderLeaf..configure() and typed .extend() contributions.