Features
- Provider Boundary: Your app owns Hocuspocus, WebSocket, WebRTC, IndexedDB, or custom transport code.
@platejs/yjsadapts aY.Doc, provider lifecycle, awareness, selection state, and undo/redo coordination to the editor. - Custom Providers: Wrap provider packages as
YjsProviderLikeand pass them toYjsPlugin.configure. - Awareness & Cursors: Integrates Yjs Awareness protocol for sharing cursor locations and other ephemeral state between users. Includes
RemoteCursorOverlayfor rendering remote cursors. - Customizable Cursors: Send cursor names, colors, and other metadata through
tx.yjs.sendSelection. - Explicit Commands: The plugin installs the Plite Yjs extension, including
state.yjsreads andtx.yjscommands for connection, awareness, selection, undo, and redo.
Manual Usage
Installation
Install the Yjs plugin.
pnpm add @platejs/yjspnpm add @platejs/yjsFor Hocuspocus server-based collaboration:
pnpm add @hocuspocus/providerpnpm add @hocuspocus/providerFor WebRTC peer-to-peer collaboration:
pnpm add y-webrtcpnpm add y-webrtcAdd The Plugin
import { YjsPlugin } from "@platejs/yjs/react";
import { createPlateEditor } from "platejs/react";
const editor = createPlateEditor({
plugins: [
YjsPlugin.configure({
options: {
clientId: "local-user",
doc,
provider,
rootName: roomId,
},
}),
],
value: initialValue,
});import { YjsPlugin } from "@platejs/yjs/react";
import { createPlateEditor } from "platejs/react";
const editor = createPlateEditor({
plugins: [
YjsPlugin.configure({
options: {
clientId: "local-user",
doc,
provider,
rootName: roomId,
},
}),
],
value: initialValue,
});App code owns the provider package, room name, authentication, persistence,
and server scaling. YjsPlugin owns Plate composition and installs the
Plite/Yjs synchronization extension.
Configure Provider And Cursors
Wrap your provider as YjsProviderLike, then pass it through the plugin's
options. The same React subpath exports remote cursor hooks.
import { useYjsRemoteCursors, YjsPlugin } from "@platejs/yjs/react";
import { RemoteCursorOverlay } from "@/components/ui/remote-cursor-overlay";
export const yjsPlugin = YjsPlugin.configure({
options: {
clientId: user.id,
doc,
provider,
rootName: roomId,
},
});import { useYjsRemoteCursors, YjsPlugin } from "@platejs/yjs/react";
import { RemoteCursorOverlay } from "@/components/ui/remote-cursor-overlay";
export const yjsPlugin = YjsPlugin.configure({
options: {
clientId: user.id,
doc,
provider,
rootName: roomId,
},
});provider: AYjsProviderLikewrapper around app-owned transport code.doc: The sharedY.Doc.rootName: The shared root inside the Yjs document.- Cursor data is sent through
tx.yjs.sendSelection(...)when the local selection changes.
Add Editor Container
The RemoteCursorOverlay requires a positioned container around the editor content. Use EditorContainer component or PlateContainer from platejs/react:
import { Plate } from "platejs/react";
import { EditorContainer } from "@/components/ui/editor";
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);import { Plate } from "platejs/react";
import { EditorContainer } from "@/components/ui/editor";
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);Read And Control Connection State
The plugin installs a yjs group in editor.read and editor.update.
const connected = editor.read((state) => state.yjs.connected());
const providerStatus = editor.read((state) => state.yjs.providerStatus());
editor.update((tx) => {
tx.yjs.connect();
tx.yjs.sendSelection(selection, { name: "Ada", color: "#aabbcc" });
});const connected = editor.read((state) => state.yjs.connected());
const providerStatus = editor.read((state) => state.yjs.providerStatus());
editor.update((tx) => {
tx.yjs.connect();
tx.yjs.sendSelection(selection, { name: "Ada", color: "#aabbcc" });
});The installed extension cleanup destroys editor-owned listeners. Provider destruction depends on whether the provider wrapper declares itself editor-owned.
Monitor Connection Status (Optional)
Access provider states and add event handlers for connection monitoring:
import React from "react";
import { useYjsProviderStatus, useYjsProviderSynced } from "@platejs/yjs/react";
function EditorStatus() {
const status = useYjsProviderStatus(editor);
const synced = useYjsProviderSynced(editor);
return (
<div>
{status} {synced ? "Synced" : "Syncing"}
</div>
);
}import React from "react";
import { useYjsProviderStatus, useYjsProviderSynced } from "@platejs/yjs/react";
function EditorStatus() {
const status = useYjsProviderStatus(editor);
const synced = useYjsProviderSynced(editor);
return (
<div>
{status} {synced ? "Synced" : "Syncing"}
</div>
);
}Provider Boundary
Provider packages stay at the app edge. Wrap Hocuspocus, WebSocket, WebRTC,
IndexedDB, or another transport as YjsProviderLike, then pass it through
YjsPlugin.configure. The wrapper owns network connection state. The plugin
installs the Plite/Yjs synchronization extension.
| Concern | Owner |
|---|---|
Y.Doc and root name | App plus YjsPlugin config |
| Provider package installation | App |
| Room naming and authentication | App |
| Persistence and server scaling | App |
| Editor operations, selection, awareness, undo/redo | @platejs/yjs |
Connection Commands
Use editor.read for provider state and editor.update for commands.
const connected = editor.read((state) => state.yjs.connected());
const synced = editor.read((state) => state.yjs.providerSynced());
editor.update((tx) => {
tx.yjs.connect();
tx.yjs.sendSelection(selection, { name: "Ada", color: "#aabbcc" });
});const connected = editor.read((state) => state.yjs.connected());
const synced = editor.read((state) => state.yjs.providerSynced());
editor.update((tx) => {
tx.yjs.connect();
tx.yjs.sendSelection(selection, { name: "Ada", color: "#aabbcc" });
});| Command | Use |
|---|---|
tx.yjs.connect() | Connect the provider. |
tx.yjs.disconnect() | Disconnect without tearing down the editor. |
tx.yjs.reconnect() | Reconnect after a provider interruption. |
tx.yjs.sendSelection(range, data) | Publish local selection and cursor metadata. |
tx.yjs.clearSelection() | Clear local awareness selection. |
tx.yjs.undo() / tx.yjs.redo() | Run Yjs-aware history. |
Backend Setup
Hocuspocus
Install and configure @hocuspocus/provider in app code. Pass its shared
Y.Doc and provider wrapper to YjsPlugin.configure. Keep auth tokens,
room names, and server URL outside the Plate package.
WebRTC
Install y-webrtc in app code when peer-to-peer collaboration is a fit. Use your
own signaling and TURN infrastructure for production. The editor package only
needs the wrapped provider and shared Y.Doc.
IndexedDB
Use y-indexeddb as local app persistence when you want a document to restore
before remote sync finishes. IndexedDB does not transport remote awareness or
cursors by itself.
Troubleshooting
- Verify every collaborator uses the same
Y.Docroot name. - Keep provider connection errors visible in app logs.
- Do not mutate the shared
Y.Docbehind the editor while the editor is mounted. - Use
tx.yjs.disconnect()for temporary disconnects and extension cleanup for unmount teardown. - If remote cursors are wrong, verify the app sends cursor metadata through
tx.yjs.sendSelection(...)and renders the React cursor hooks from@platejs/yjs/react.