Back to Plate

Plate Controller

content/docs/api/core/plate-controller.mdx

53.0.85.5 KB
Original Source

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.

Quick Use

Wrap the shared UI and all editors in PlateController. PlateContent registers each mounted editor store through PlateControllerEffect.

tsx
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.

Active Editor Lookup

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.

LookupBehavior
useEditorRef('main')Resolves the store registered for main.
useEditorRef()Resolves the active editor store, then the first mounted primary editor store.
Missing store with controllerReturns the fallback store, so useEditorRef() returns a fallback editor.
Missing store without controllerThrows Plate hooks must be used inside a Plate or PlateController.

Controller lookup order without an explicit ID:

  1. activeId
  2. each ID in primaryEditorIds
  3. fallback store when no store is available

Fallback Editors

The fallback editor exists so read-only UI can render while no editor is active. It is not safe for transforms.

tsx
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>;
}
<Callout type="warning" title="Guard transforms"> Check `useEditorMounted(id?)` or `!editor.meta.isFallback` before running transforms from UI that lives under `PlateController`. </Callout>

Registration

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.

StateOwnerBehavior
editorStoresPlateControllerEffectMaps mounted editor IDs to their Jotai stores. Unmounted IDs are set to null.
primaryEditorIdsPlateControllerEffectAppends mounted editors whose Plate store has primary: true; removes them on unmount.
activeIdPlateControllerEffectSet to the focused editor ID. Cleared on unmount when the unmounted editor was active.

API Reference

PlateController

Provider 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>

Controller Store State

StateTypeDefault
activeIdstring | nullnull
editorStoresRecord<string, JotaiStore | null>{}
primaryEditorIdsstring[][]

usePlateControllerStore

Resolve 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>

usePlateControllerExists

Check whether a local controller provider exists.

<API name="usePlateControllerExists"> <APIReturns type="boolean"> `true` when `usePlateControllerLocalStore()` finds a controller store. </APIReturns> </API>

usePlateControllerLocalStore

Read 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>

PlateControllerEffect

Register a Plate store with the nearest controller.

<API name="PlateControllerEffect"> <APIProps> <APIItem name="id" type="string" optional> Editor ID to register. Defaults to the ID from the current Plate store. </APIItem> </APIProps> </API>

PlateContent renders PlateControllerEffect for you. Render it directly only when you build a custom content surface that still needs controller registration.