Plate keeps Plite's document model and moves editor setup, rendering, handlers, and command wiring into plugins. Migrate the editor shell first, then move custom rendering and behavior into plugins.
Install
pnpm add platejspnpm add platejsUse 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.
Migration Map
| 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 .withComponent() or node.component. |
withX(editor) plugin functions | .extendApi() for read/services, .extendTx() for plugin commands, .extendTxGroup() for shared command groups, or .overrideEditor() only when wrapping an existing editor.api method. |
Top-level event handlers on Editable | Plugin handlers or shortcuts. |
Transforms.* imports | editor.update((tx) => tx.*). |
Editor.* imports | editor.read((state) => state.*), or a plugin-owned editor.api.* service. |
Editor Shell
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: 'p',
},
];
export function Editor() {
const editor = usePlateEditor({
value: 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: 'p',
},
];
export function Editor() {
const editor = usePlateEditor({
value: 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({
value: [
{
children: [{ text: 'Draft' }],
type: 'p',
},
],
});import { createPlateEditor } from 'platejs/react';
export const editor = createPlateEditor({
value: [
{
children: [{ text: 'Draft' }],
type: 'p',
},
],
});Custom Elements
Replace renderElement branches with node plugins. Use .withComponent() when the only change is the React component.
import {
ParagraphPlugin,
PlateElement,
type PlateElementProps,
} from 'platejs/react';
export function ParagraphElement({
children,
...props
}: PlateElementProps) {
return (
<PlateElement className="m-0 px-0 py-1" {...props}>
{children}
</PlateElement>
);
}
export const AppParagraphPlugin = ParagraphPlugin.withComponent(
ParagraphElement
);import {
ParagraphPlugin,
PlateElement,
type PlateElementProps,
} from 'platejs/react';
export function ParagraphElement({
children,
...props
}: PlateElementProps) {
return (
<PlateElement className="m-0 px-0 py-1" {...props}>
{children}
</PlateElement>
);
}
export const AppParagraphPlugin = ParagraphPlugin.withComponent(
ParagraphElement
);If your Plite document stores a custom type like paragraph, keep that type on the plugin.
export const AppParagraphPlugin = ParagraphPlugin.configure({
node: { type: 'paragraph' },
}).withComponent(ParagraphElement);export const AppParagraphPlugin = ParagraphPlugin.configure({
node: { type: 'paragraph' },
}).withComponent(ParagraphElement);Custom Behavior
Use .extendTx() when a plugin owns a reusable document command. The command is
available inside editor.update(...) under the plugin key.
import { createPlatePlugin } from 'platejs/react';
export const SignaturePlugin = createPlatePlugin({
key: 'signature',
}).extendTx(() => (tx) => ({
insert() {
tx.text.insert(' - Plate');
},
}));import { createPlatePlugin } from 'platejs/react';
export const SignaturePlugin = createPlatePlugin({
key: 'signature',
}).extendTx(() => (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();
});Use .extendTxGroup() when a plugin adds a command to a shared command group.
That keeps related commands together without making a global alias.
import { createPlatePlugin } from 'platejs/react';
export const CalloutPlugin = createPlatePlugin({
key: 'callout',
}).extendTxGroup('insert', () => (tx) => ({
callout() {
tx.nodes.insert({
children: [{ text: '' }],
type: 'callout',
});
},
}));import { createPlatePlugin } from 'platejs/react';
export const CalloutPlugin = createPlatePlugin({
key: 'callout',
}).extendTxGroup('insert', () => (tx) => ({
callout() {
tx.nodes.insert({
children: [{ text: '' }],
type: 'callout',
});
},
}));editor.update((tx) => {
tx.insert.callout();
});editor.update((tx) => {
tx.insert.callout();
});Use .extendApi() for editor-scoped read or service methods. Use
.overrideEditor() only when you need to wrap an existing editor.api method
and keep access to the original implementation.
Handlers And Shortcuts
Move editor events into the plugin that owns the behavior.
import { createPlatePlugin } from 'platejs/react';
export const TabPlugin = createPlatePlugin({
key: 'tab',
handlers: {
onKeyDown: ({ event }) => {
if (event.key !== 'Tab') return false;
event.preventDefault();
return true;
},
},
});import { createPlatePlugin } from 'platejs/react';
export const TabPlugin = createPlatePlugin({
key: 'tab',
handlers: {
onKeyDown: ({ 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 { createPlatePlugin } from 'platejs/react';
export const SavePlugin = createPlatePlugin({
key: 'save',
}).extend({
shortcuts: {
draft: {
keys: 'mod+s',
handler: ({ event }) => {
event.preventDefault();
return true;
},
},
},
});import { createPlatePlugin } from 'platejs/react';
export const SavePlugin = createPlatePlugin({
key: 'save',
}).extend({
shortcuts: {
draft: {
keys: 'mod+s',
handler: ({ event }) => {
event.preventDefault();
return true;
},
},
},
});API Calls
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);Headless Code
Use createBaseEditor from platejs for non-React importers, serializers, transforms, and tests.
import { createBaseEditor } from 'platejs';
export const editor = createBaseEditor({
value: [
{
children: [{ text: 'Headless document.' }],
type: 'p',
},
],
});import { createBaseEditor } from 'platejs';
export const editor = createBaseEditor({
value: [
{
children: [{ text: 'Headless document.' }],
type: 'p',
},
],
});Related
- Editor for editor creation options.
- Plugin Components for replacing
renderElementandrenderLeaf. - Plugin Methods for
.configure(),.extend*(), and.overrideEditor(). - Plugin Shortcuts for keyboard command wiring.