Collaboration

PreviousNext

Real-time collaboration with Yjs

Loading…

Features

  • Provider Boundary: Your app owns Hocuspocus, WebSocket, WebRTC, IndexedDB, or custom transport code. @platejs/yjs adapts a Y.Doc, provider lifecycle, awareness, selection state, and undo/redo coordination to the editor.
  • Custom Providers: Wrap provider packages as YjsProviderLike and pass them to YjsPlugin.configure.
  • Awareness & Cursors: Integrates Yjs Awareness protocol for sharing cursor locations and other ephemeral state between users. Includes RemoteCursorOverlay for 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.yjs reads and tx.yjs commands for connection, awareness, selection, undo, and redo.

Manual Usage

Installation

Install the Yjs plugin.

pnpm add @platejs/yjs
pnpm add @platejs/yjs

For Hocuspocus server-based collaboration:

pnpm add @hocuspocus/provider
pnpm add @hocuspocus/provider

For WebRTC peer-to-peer collaboration:

pnpm add y-webrtc
pnpm add y-webrtc

Add 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,
});

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: A YjsProviderLike wrapper around app-owned transport code.
  • doc: The shared Y.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.

ConcernOwner
Y.Doc and root nameApp plus YjsPlugin config
Provider package installationApp
Room naming and authenticationApp
Persistence and server scalingApp
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" });
});
CommandUse
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.Doc root name.
  • Keep provider connection errors visible in app logs.
  • Do not mutate the shared Y.Doc behind 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.