content/docs/api/core/plate-controller.mdx
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 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 useEditorRef() and useEditorMounted() normally read the nearest Plate store. Inside PlateController, the same hooks can resolve a store outside a specific editor tree.
| Lookup | Behavior |
|---|---|
useEditorRef('main') | Resolves the store registered for main. |
useEditorRef() | Resolves the active editor store, then the first mounted primary editor store. |
| Missing store with controller | Returns the fallback store, so useEditorRef() returns a fallback editor. |
| Missing store without controller | Throws Plate hooks must be used inside a Plate or PlateController. |
Controller lookup order without an explicit ID:
activeIdprimaryEditorIdsThe fallback editor exists so read-only UI can render while no editor is active. It is not safe for transforms.
import { useEditorMounted, useEditorRef } from 'platejs/react';
export function ActiveEditorLabel() {
const editor = useEditorRef();
const mounted = useEditorMounted();
if (!mounted || editor.meta.isFallback) {
return <p>No editor selected.</p>;
}
return <p>Active editor: {editor.id}</p>;
}
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 Slate 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. |
PlateControllerProvider for cross-editor lookup state.
<API name="PlateController"> <APIProps> <APIItem name="children" type="React.ReactNode"> Shared UI and editor trees that should participate in controller lookup. </APIItem> <APIItem name="activeId" type="string | null" optional> Initial active editor ID. </APIItem> <APIItem name="editorStores" type="Record<string, JotaiStore | null>" optional> Initial editor-store map. </APIItem> <APIItem name="primaryEditorIds" type="string[]" optional> Initial primary editor ID list. </APIItem> </APIProps> </API>| State | Type | Default |
|---|---|---|
activeId | string | null | null |
editorStores | Record<string, JotaiStore | null> | {} |
primaryEditorIds | string[] | [] |
usePlateControllerStoreResolve a Plate Jotai store from the controller.
<API name="usePlateControllerStore"> <APIParameters> <APIItem name="idProp" type="string" optional> Editor ID to resolve directly. </APIItem> </APIParameters> <APIReturns type="JotaiStore | null"> Matching editor store, active editor store, first mounted primary editor store, or `null`. </APIReturns> </API>usePlateControllerExistsCheck whether a local controller provider exists.
<API name="usePlateControllerExists"> <APIReturns type="boolean"> `true` when `usePlateControllerLocalStore()` finds a controller store. </APIReturns> </API>usePlateControllerLocalStoreRead the local controller atom store.
<API name="usePlateControllerLocalStore"> <APIParameters> <APIItem name="options" type="string | { scope?: string; warnIfNoStore?: boolean }" optional> Scope options passed to the generated controller store hook. A string is treated as `scope`. </APIItem> </APIParameters> <APIReturns type="PlateControllerStore"> Local controller store hook result. </APIReturns> </API>PlateControllerEffectRegister 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.