PlateController lets UI outside a single <Plate> subtree read the active editor store. Use it for shared toolbars, side panels, inspectors, and multi-editor shells.
Wrap the shared UI and all editors in PlateController. PlateContent registers each mounted editor store through PlateControllerEffect.
import {
Plate,
PlateContent,
PlateController,
usePlateEditor,
} from 'platejs/react';
export function
import {
Plate,
PlateContent,
PlateController,
usePlateEditor,
} from 'platejs/react';
export function EditorShell() {
return (
<PlateController>
<ActiveEditorLabel />
<MainEditor />
<SecondaryEditor />
</PlateController>
);
}
function MainEditor() {
const editor = usePlateEditor({
id: 'main' });
return (
<Plate editor={editor}>
<PlateContent />
</Plate>
);
}
function SecondaryEditor() {
const editor = usePlateEditor({
id: 'secondary' });
return (
<Plate editor={editor} primary={false}>
<PlateContent />
</Plate>
);
}primary belongs on Plate, not on createPlateEditor or usePlateEditor.
Hooks such as useEditor() and useEditorMounted() normally read through the
nearest Plate store. Inside PlateController, the same hooks can resolve an
editor outside a specific editor tree.
| Lookup | Behavior |
|---|---|
useEditor({ id: 'main' }) | Resolves the active editor registered for main; throws when it is absent. |
useEditor() | Resolves the active editor, then the first mounted primary editor; throws when none exists. |
useActiveEditor() | Uses the same lookup and returns null when no editor is active. |
| Missing store without controller | Throws Plate hooks must be used inside a Plate or PlateController. |
Controller lookup order without an explicit ID:
activeIdprimaryEditorIdsPlate keeps an inert editor internally so hook subscriptions retain a stable
call shape. Public code never receives it. Use useActiveEditor() for UI that
can render while no editor is active; use useEditor() when absence is a
programming error.
import { useActiveEditor } from 'platejs/react';
export function ActiveEditorLabel() {
const editor = useActiveEditor();
if (!editor) return <p>No editor selected.</p>;
return <p>Active editor: {editor.id}</p>;
}import { useActiveEditor } from 'platejs/react';
export function ActiveEditorLabel() {
const editor = useActiveEditor();
if (!editor) return <p>No editor selected.</p>;
return <p>Active editor: {editor.id}</p>;
}useEditor() is the shorter path for commands and components that require an
active editor because it fails at the missing provider instead of letting a
mutation disappear into fallback state.
PlateControllerEffect runs inside PlateContent. It registers the current Plate store by editor ID, appends primary editors to primaryEditorIds, removes them on unmount, and sets activeId when Plite focus enters that editor.
| State | Owner | Behavior |
|---|---|---|
editorStores | PlateControllerEffect | Maps mounted editor IDs to their Jotai stores. Unmounted IDs are set to null. |
primaryEditorIds | PlateControllerEffect | Appends mounted editors whose Plate store has primary: true; removes them on unmount. |
activeId | PlateControllerEffect | Set to the focused editor ID. Cleared on unmount when the unmounted editor was active. |
Provider for cross-editor lookup state.
| State | Type | Default |
|---|---|---|
activeId | string | null | null |
editorStores | Record<string, JotaiStore | null> | {} |
primaryEditorIds | string[] | [] |
Resolve a Plate Jotai store from the controller.
Check whether a local controller provider exists.
Read the local controller atom store.
Register a Plate store with the nearest controller.
PlateContent renders PlateControllerEffect for you. Render it directly only when you build a custom content surface that still needs controller registration.