Skip to main content
Yoopta Editor supports real-time collaborative editing using Yjs and WebSocket connections. Multiple users can edit the same document simultaneously with conflict-free merges and live cursor positions.

Installation

npm install @yoopta/collaboration yjs

Quick Start

Wrap your editor with the withCollaboration extension to enable real-time collaboration:
import { createYooptaEditor } from '@yoopta/editor';
import { withCollaboration, RemoteCursors } from '@yoopta/collaboration';
import type { CollaborationUser } from '@yoopta/collaboration';

const user: CollaborationUser = {
  id: 'user-123',
  name: 'John Doe',
  color: '#FF6B6B',
  avatar: 'https://example.com/avatar.jpg',
};

const editor = useMemo(() => {
  const base = createYooptaEditor({
    plugins: PLUGINS,
    marks: MARKS,
  });

  return withCollaboration(base, {
    url: 'wss://your-server.com',
    roomId: 'document-123',
    user,
    connect: false, // Connect manually later
  });
}, []);

Configuration

The withCollaboration function accepts a configuration object:
type CollaborationConfig = {
  /** WebSocket server URL */
  url: string;
  
  /** Room/document identifier */
  roomId: string;
  
  /** User info for awareness (cursors, presence) */
  user: CollaborationUser;
  
  /** Optional: provide your own Y.Doc instance */
  document?: YDoc;
  
  /** Initial value to seed the document if no remote state exists */
  initialValue?: YooptaContentValue;
  
  /** Whether to connect immediately (default: true) */
  connect?: boolean;
  
  /** Authentication token sent to the server on connect */
  token?: string;
};

Connection Management

Control the connection lifecycle manually:
function CollaborationEditor() {
  const editor = useMemo(() => {
    const base = createYooptaEditor({ plugins, marks });
    return withCollaboration(base, {
      url: WS_URL,
      roomId: ROOM_ID,
      user,
      connect: false, // Don't auto-connect
    });
  }, []);

  useEffect(() => {
    // Connect when component mounts
    editor.collaboration.connect();

    return () => {
      // Disconnect when component unmounts
      editor.collaboration.disconnect();
    };
  }, [editor.collaboration]);

  return <YooptaEditor editor={editor} />;
}

Collaboration API

The collaboration extension adds a collaboration namespace to the editor:
interface CollaborationAPI {
  /** Current collaboration state */
  readonly state: CollaborationState;
  
  /** Connect to the collaboration server */
  connect: () => void;
  
  /** Disconnect from the collaboration server */
  disconnect: () => void;
  
  /** Clean up all collaboration resources */
  destroy: () => void;
  
  /** Get the Yjs document */
  getDocument: () => YDoc;
}

Connection State

type CollaborationState = {
  status: 'disconnected' | 'connecting' | 'connected' | 'error';
  connectedUsers: CollaborationUser[];
  document: YDoc | null;
  isSynced: boolean;
};

React Hooks

useCollaboration

Access the current collaboration state:
import { useCollaboration } from '@yoopta/collaboration';

function CollaborationStatus() {
  const { status, connectedUsers, isSynced } = useCollaboration();

  return (
    <div>
      <span>Status: {status}</span>
      <span>Users: {connectedUsers.length}</span>
      <span>Synced: {isSynced ? 'Yes' : 'No'}</span>
    </div>
  );
}

useConnectionStatus

Monitor just the connection status:
import { useConnectionStatus } from '@yoopta/collaboration';

function ConnectionIndicator() {
  const status = useConnectionStatus();
  
  return (
    <div className={status === 'connected' ? 'online' : 'offline'}>
      {status}
    </div>
  );
}

useRemoteCursors

Access remote cursor positions:
import { useRemoteCursors } from '@yoopta/collaboration';

function CursorDebug() {
  const cursors = useRemoteCursors();
  
  return (
    <div>
      {cursors.map((cursor) => (
        <div key={cursor.clientId}>
          {cursor.user.name} - Block: {cursor.blockId}
        </div>
      ))}
    </div>
  );
}

Remote Cursors

Show live cursor positions of other users:
1

Import the component

import { RemoteCursors } from '@yoopta/collaboration';
2

Add to editor

<YooptaEditor editor={editor}>
  <RemoteCursors />
  {/* other UI components */}
</YooptaEditor>
The RemoteCursors component automatically displays:
  • Live cursor positions with user colors
  • User names on hover
  • Selection ranges for multi-block selections

Complete Example

Here’s a full collaborative editor implementation:
import { useCallback, useEffect, useMemo, useRef } from 'react';
import YooptaEditor, { createYooptaEditor } from '@yoopta/editor';
import { withCollaboration, RemoteCursors } from '@yoopta/collaboration';
import type { CollaborationUser } from '@yoopta/collaboration';

const WS_URL = 'wss://cloud.yoopta.dev';
const ROOM_ID = 'my-document';

function CollaborationEditor() {
  const user: CollaborationUser = {
    id: crypto.randomUUID(),
    name: 'Current User',
    color: '#FF6B6B',
  };

  const editor = useMemo(() => {
    const base = createYooptaEditor({
      plugins: PLUGINS,
      marks: MARKS,
    });

    return withCollaboration(base, {
      url: WS_URL,
      roomId: ROOM_ID,
      user,
      connect: false,
    });
  }, []);

  useEffect(() => {
    editor.collaboration.connect();

    return () => {
      editor.collaboration.disconnect();
    };
  }, [editor.collaboration]);

  return (
    <YooptaEditor
      editor={editor}
      placeholder="Start typing to collaborate..."
    >
      <RemoteCursors />
    </YooptaEditor>
  );
}

Undo/Redo with Collaboration

When collaboration is enabled, the undo/redo functionality automatically uses Yjs’s UndoManager, which only undoes local changes:
// These now use Y.UndoManager under the hood
editor.undo();  // Only undoes your changes
editor.redo();  // Only redoes your changes
Remote changes from other users are never undone, preventing conflicts.

Events

Listen to collaboration events:
// State changes
editor.on('collaboration:state-change', (state) => {
  console.log('State:', state.status, state.connectedUsers);
});

// Connection status changes
editor.on('collaboration:status-change', ({ status }) => {
  console.log('Status:', status);
});

// Cursor changes
editor.on('collaboration:cursors-change', (cursors) => {
  console.log('Cursors:', cursors);
});

Server Setup

You’ll need a WebSocket server that supports Yjs. The package includes a WebSocketProvider compatible with standard Yjs servers. Example server setup with y-websocket:
npm install y-websocket
const WebSocket = require('ws');
const { setupWSConnection } = require('y-websocket/bin/utils');

const wss = new WebSocket.Server({ port: 1234 });

wss.on('connection', (conn, req) => {
  setupWSConnection(conn, req);
});

Advanced: Custom Y.Doc

Provide your own Yjs document for advanced use cases:
import * as Y from 'yjs';

const doc = new Y.Doc();

// Add custom shared types
const customData = doc.getMap('custom');

const editor = withCollaboration(base, {
  url: WS_URL,
  roomId: ROOM_ID,
  user,
  document: doc, // Use custom doc
});

// Access the document
const yDoc = editor.collaboration.getDocument();

Best Practices

Use useEffect cleanup functions to disconnect when components unmount:
useEffect(() => {
  editor.collaboration.connect();
  return () => editor.collaboration.disconnect();
}, []);
Monitor the connection status and show appropriate UI:
const { status } = useCollaboration();

if (status === 'error') {
  return <div>Failed to connect. Please refresh.</div>;
}
Generate stable user IDs that persist across sessions:
const userId = localStorage.getItem('userId') || crypto.randomUUID();
localStorage.setItem('userId', userId);
Seed the document with initial content for new rooms:
withCollaboration(editor, {
  // ...
  initialValue: INITIAL_CONTENT,
});

Troubleshooting

Users not seeing changes: Ensure all clients are connected to the same roomId. Cursors not showing: Make sure <RemoteCursors /> is inside <YooptaEditor>. Connection drops: Check WebSocket server is running and accessible. Sync issues: Verify the isSynced flag before considering the document ready.

Build docs developers (and LLMs) love