Plugin authors put independent fields in the plugin constructor. Apps and
registries finish a descriptor with one terminal .configure() call, including
component when it renders an ordinary node.
| Method | Use it for | Type behavior |
|---|---|---|
.extend() | Adapt an imported/prebuilt descriptor, use a constructor-inaccessible shared factory, or consume an earlier-stage type. | Widens the plugin type. |
.configure() | Override existing fields for one consumer. | Terminal and non-widening. |
defineBasePlugin() and definePlatePlugin() own every independent author
contribution:
| Field | Owns |
|---|---|
initialState | The seed for this plugin's editor-local store. |
api | Factory for immutable plugin-scoped services. |
read | Snapshot or transaction-local reads. |
selectors | Pure state-first derivations, including React subscriptions. |
update | Transaction-bound plugin mutations. |
| Native Plite fields | Read middleware, commands, corrections, state/effect/facet/selection descriptors, contributions, lifecycle, activation, and validation. |
codecs | Plugin-owned format decoding and encoding. |
Callbacks receive the inferred Plugin Context, including
editor, plugin, api, read, defineCodecs, store, and the active tx
inside update contributions.
Put independent fields and their context callbacks in the constructor. Use
.extend() for an imported/prebuilt declaration or types introduced by an
earlier stage.
import { definePlatePlugin, usePluginStore } from 'platejs/react';
export type CounterPluginState = {
value: number;
};
export const CounterPlugin = definePlatePlugin('counter', {
api: ({ store }) => ({
isEmpty: () => store.get('value') === 0,
}),
initialState: {
value: 0,
} satisfies CounterPluginState,
selectors: {
doubled: (state, factor: number) => state.value * factor,
isEven: (state) => state.value % 2 === 0,
},
update: ({ store, tx }) => ({
insertLabel: () => {
tx.text.insert(`Count: ${store.get('value')}`);
},
}),
});
export function CounterValue() {
const doubled = usePluginStore(CounterPlugin, 'doubled', 2);
const isEven = usePluginStore(CounterPlugin, 'isEven');
return (
<span>
{doubled} / {isEven ? 'even' : 'odd'}
</span>
);
}import { definePlatePlugin, usePluginStore } from 'platejs/react';
export type CounterPluginState = {
value: number;
};
export const CounterPlugin = definePlatePlugin('counter', {
api: ({ store }) => ({
isEmpty: () => store.get('value') === 0,
}),
initialState: {
value: 0,
} satisfies CounterPluginState,
selectors: {
doubled: (state, factor:
Let the constructor infer contributed API, read, selector, and update groups. Keep an exported state type only when package consumers need that contract.
After resolution, concrete editors expose the plugin API under its readable name. Generic package code uses the typed portal.
editor.api.counter.isEmpty();
editor.plugin(CounterPlugin).api.isEmpty();
editor.plugin(CounterPlugin).update.insertLabel();editor.api.counter.isEmpty();
editor.plugin(CounterPlugin).api.isEmpty();
editor.plugin(CounterPlugin).update.insertLabel();Inside a later update contribution, reuse an earlier update through the active transaction group:
export const CounterPairPlugin = CounterPlugin.extend(() => ({
update: ({ tx }) => ({
insertPair: () => {
tx.plugin(CounterPlugin).insertLabel();
tx.plugin(CounterPlugin).insertLabel();
},
}),
}));export const CounterPairPlugin = CounterPlugin.extend(() => ({
update: ({ tx }) => ({
insertPair: () => {
tx.plugin(CounterPlugin).insertLabel();
tx.plugin(CounterPlugin).insertLabel();
},
}),
}));Calling a portal one-shot update there would open a nested transaction.
Declare generic editor substrate directly on the plugin: schema, commands, corrections, read middleware, state/effect/facet/selection descriptors, contributions, lifecycle, activation, and validation.
import { defineBasePlugin, editorCommands } from 'platejs';
export const SingleLinePlugin = defineBasePlugin('singleLine', {
commands: ({ handle }) => [
handle(editorCommands.insertBreak, ({ state }) =>
state.transaction(() => {})
),
],
});import { defineBasePlugin, editorCommands } from 'platejs';
export const SingleLinePlugin = defineBasePlugin('singleLine', {
commands: ({ handle }) => [
handle(editorCommands.insertBreak, ({ state }) =>
state.transaction(() => {})
),
],
});Native callbacks receive only their Plite capability context. When one needs
an earlier plugin-owned store, read, API, or update group, add one staged
.extend() and close over that capability. Do not pass owner context through a
new helper parameter:
import { defineBasePlugin, editorCommands } from 'platejs';
export const TriggerPlugin = defineBasePlugin('trigger', {
initialState: { enabled: true },
}).extend(({ store }) => ({
commands: ({ handle }) => [
handle(editorCommands.insertBreak, ({ state }) => {
if (!store.get('enabled')) return false;
return state.transaction(() => {});
}),
],
}));import { defineBasePlugin, editorCommands } from 'platejs';
export const TriggerPlugin = defineBasePlugin('trigger', {
initialState: { enabled: true },
}).extend(({ store }) => ({
commands: ({ handle }) => [
handle(editorCommands.insertBreak, ({ state }) => {
if (!store.get('enabled')) return false;
return state.transaction(() => {});
}),
],
}));For an independently reusable Plite descriptor, import
defineExtension from @platejs/plite and list the descriptor in
dependencies.
Plugin-specific and editor-wide host services share the root api channel;
Plite projects that API under the plugin name. Do not publish the same
implementation twice.
Build the MIME-keyed map with the callback's defineCodecs. This is the one
inline inference anchor for codec callbacks.
import { ContentSlice } from '@platejs/plite';
import { defineBasePlugin } from 'platejs';
export const RecordsPlugin = defineBasePlugin('records', {
codecs: ({ defineCodecs }) =>
defineCodecs({
'application/json': {
scope: 'document',
decode: ({ data }) => ContentSlice.fromJSON(JSON.parse(data)),
encode: ({ slice }) => JSON.stringify(slice),
},
}),
});import { ContentSlice } from '@platejs/plite';
import { defineBasePlugin } from 'platejs';
export const RecordsPlugin = defineBasePlugin('records', {
codecs: ({ defineCodecs }) =>
defineCodecs({
'application/json': {
scope: 'document',
decode: ({ data }) => ContentSlice.fromJSON(JSON.parse(data)),
encode: ({ slice }) => JSON.stringify(slice),
},
}),
});Use document scope only when the format represents the complete document.
Use defineCodecs(map) for self and product codecs. Use
defineCodecs(TargetPlugin, map) for a foreign codec; the helper injects the
target into every HTML rule. The map's 'text/html' value accepts one
schema-aware rule or a non-empty ordered rule tuple when the plugin owns
multiple HTML representations. Keep that tuple in the same map. Do not author
a direct codec map or add target manually.
Bind an ordinary node component in the same terminal .configure() call as
the descriptor's other consumer overrides.
import { ParagraphPlugin } from 'platejs/react';
import { ParagraphElement } from '@/components/editor/paragraph';
export const AppParagraphPlugin = ParagraphPlugin.configure({
component: ParagraphElement,
shortcuts: {
toggle: { keys: 'mod+alt+0' },
},
});import { ParagraphPlugin } from 'platejs/react';
import { ParagraphElement } from '@/components/editor/paragraph';
export const AppParagraphPlugin = ParagraphPlugin.configure({
component: ParagraphElement,
shortcuts: {
toggle: { keys: 'mod+alt+0' },
},
});Component binding preserves the plugin type. Do not assign the node component through a renderer registry field.
Use .configure() once, where an app or registry installs a plugin, to change
fields already declared by the author.
import { LinkPlugin } from '@platejs/link/react';
export const AppLinkPlugin = LinkPlugin.configure({
initialState: {
allowedSchemes: ['http', 'https', 'mailto'],
},
});import { LinkPlugin } from '@platejs/link/react';
export const AppLinkPlugin = LinkPlugin.configure({
initialState: {
allowedSchemes: ['http', 'https', 'mailto'],
},
});Object configs use Plate's merge rules: objects merge deeply, arrays replace,
and initialState shallow-merges. .configure() cannot publish a new capability and
must be the final call.
Configure a required dependency on its own descriptor and place that complete descriptor beside the owner in the app or registry plugin array.
type CellPluginState = {
padding: number;
};
const CellPlugin = definePlatePlugin('cell', {
initialState: {
padding: 12,
} satisfies CellPluginState,
});
const GridPlugin = definePlatePlugin('grid', {
dependencies: [CellPlugin],
});
export const AppGridPlugins = [
GridPlugin,
CellPlugin.configure({ initialState: { padding: 8 } }),
];type CellPluginState = {
padding: number;
};
const CellPlugin = definePlatePlugin('cell', {
initialState: {
padding: 12,
} satisfies CellPluginState,
});
const GridPlugin = definePlatePlugin('grid', {
dependencies: [CellPlugin],
});
export const AppGridPlugins = [
GridPlugin,
CellPlugin.configure({ initialState: { padding: 8 } }),
];Optional capabilities are ordinary array entries. Terminal configurations derived from the same authored plugin compose in array order: earlier fields survive unless a later configuration defines the same field. Unrelated plugins and divergent authoring branches cannot share a name.
Use toPlatePlugin() in the owning React entrypoint to publish a reusable
Plate descriptor or add Plate-only behavior. App consumers do not insert a
conversion merely to set a component.
import { BaseScriptPlugin } from '@platejs/basic-nodes';
import { PlateLeaf, toPlatePlugin } from 'platejs/react';
export const ScriptPlugin = toPlatePlugin(BaseScriptPlugin, {
component: (props) => (
<PlateLeaf
{...props}
as={props.leaf.script === 'sub' ? 'sub' : 'sup'}
>
{props.children}
</PlateLeaf>
),
});import { BaseScriptPlugin } from '@platejs/basic-nodes';
import { PlateLeaf, toPlatePlugin } from 'platejs/react';
export const ScriptPlugin = toPlatePlugin(BaseScriptPlugin, {
component: (props) => (
<PlateLeaf
{...props}
as={props.leaf.script === 'sub' ? 'sub' : 'sup'}
>
{props.children}
</PlateLeaf>
),
});The package's React owner publishes ScriptPlugin; static/RSC code can bind a
server-safe component directly on BaseScriptPlugin without importing
platejs/react.