The preview runs two independent editors through a credential-free local room. It demonstrates remote document and cursor updates, disconnect/reconnect, schema rejection and recovery, and Plite History undo after a remote edit. Apps still own production transport, authentication, persistence, and room naming.
@platejs/yjs adapts a Y.Doc, provider lifecycle, awareness, selection state, and canonical editor changes.YjsProviderLike and pass them to YjsPlugin.configure.RemoteCursorOverlay for rendering remote cursors.tx.yjs.sendSelection.state.yjs reads and tx.yjs commands for connection, awareness, and selection.Install the Yjs plugin.
For Hocuspocus server-based collaboration:
For WebRTC peer-to-peer collaboration:
Every peer in a room must use the same compiled schema identity. Derived schemas
match by fingerprint. Use an explicit id and version for a durable room
lineage, and migrate the room before bumping the version.
import { YjsPlugin } from "@platejs/yjs/plate";
import { createPlateEditor } from "platejs/react";
const editor = createPlateEditor({
schema: { id: "yjs-example", version: 1 },
plugins: [
YjsPlugin.configure({
initialState: {
clientId: "local-user",
doc,
provider,
rootName: roomId,
},
}),
],
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.
Wrap your provider as YjsProviderLike, then pass it through the plugin's
initialState. The Plate subpath exports the plugin; the React subpath exports
remote cursor hooks.
import { YjsPlugin } from "@platejs/yjs/plate";
import { useYjsRemoteCursors } from "@platejs/yjs/react";
import { RemoteCursorOverlay } from "@/components/editor/remote-cursor-overlay";
export const yjsPlugin = YjsPlugin.configure({
initialState: {
clientId: user.id,
doc,
provider,
rootName: roomId,
},
});import { YjsPlugin } from
provider: A YjsProviderLike wrapper around app-owned transport code.doc: The shared Y.Doc.rootName: The collaboration document namespace. Its primary and named roots
synchronize together.collab: "shared" effect descriptors belong to installed editor
extensions. YjsPlugin discovers them from the editor.tx.yjs.sendSelection(...) when the local
selection changes.One Plate commit that changes primary and named roots becomes one Yjs
transaction. Plite History records that editor commit as one history batch.
Named roots synchronize without a mounted PlateContent, and remote selections
preserve their root key. A selection cannot span two roots.
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/editor/editor";
return (
<Plate editor={editor}>
<EditorContainer>
<Editor />
</EditorContainer>
</Plate>
);import { Plate } from "platejs/react";
import { EditorContainer } from
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" });
});
The installed extension cleanup destroys editor-owned listeners. Provider destruction depends on whether the provider wrapper declares itself editor-owned.
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>
);
}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, undo/redo | @platejs/plite and @platejs/plite-history |
| Awareness and remote cursors | @platejs/yjs |
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. |
Install Plite history for undo and redo. Its canonical replay flows through the same Yjs synchronization bridge as every other editor update:
editor.update.history.undo();
editor.update.history.redo();editor.update.history.undo();
editor.update.history.redo();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.
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.
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.
Y.Doc root name.Y.Doc behind the editor while the editor is mounted.tx.yjs.disconnect() for temporary disconnects and extension cleanup for
unmount teardown.tx.yjs.sendSelection(...) and renders the React cursor hooks from
@platejs/yjs/react.import { YjsPlugin } from "@platejs/yjs/plate";
import { createPlateEditor } from "platejs/react";
const editor = createPlateEditor({
schema: { id: "yjs-example", version: 1 },
plugins: [
YjsPlugin.configure({
initialState: {
clientId: "local-user",
doc,
provider,
rootName: roomId,
},
}),
],
initialValue,
});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>
);
}