Skip to main content
โšก Calmops

Yjs and CRDTs Complete Guide: Real-Time Collaboration Made Simple

Introduction

Building real-time collaborative applications like Google Docs or Figma is notoriously difficult. CRDTs (Conflict-free Replicated Data Types) and Yjs make this achievable without complex operational transformation algorithms. This guide covers everything you need to know.

Understanding CRDTs

The Collaboration Problem

When multiple users edit simultaneously, conflicts arise:

graph LR
    UserA[User A: "Hello"] -->|edit| Doc1[(Document)]
    UserB[User B: "World"] -->|edit| Doc1
    Doc1 -->|conflict| Issue[โŒ Conflict!]

What are CRDTs?

CRDTs are data structures that can be merged automatically, even when edited concurrently:

Approach Description Complexity
Locking One user at a time Simple, blocking
OT (Operational Transform) Transform operations Complex
CRDT Mathematically mergeable Moderate
graph LR
    UserA[User A: "Hello"] --> Doc[(Yjs Doc)]
    UserB[User B: "World"] --> Doc
    Doc -->|auto-merge| Result[โœ… "Hello World"]

Types of CRDTs

Type Use Case Examples
G-Counter Counters Likes, votes
LWW-Register Single values User name, settings
OR-Set Sets Tags, shared items
RGA Sequences Text, lists

Yjs Deep Dive

What is Yjs?

Yjs is a high-performance CRDT implementation:

  • Works in browser, Node.js, Bun, Deno
  • Framework integrations (React, Vue, Svelte)
  • Persistence providers
  • Network providers
// Simple Yjs usage
import * as Y from 'yjs';

const ydoc = new Y.Doc();

// Get or create a shared type
const ytext = ydoc.getText('message');

// Observe changes
ytext.observe(event => {
  console.log('Text changed:', ytext.toString());
});

// Make changes
ytext.insert(0, 'Hello');
ytext.insert(5, ' World');

Shared Types

// Text - for editors
const ytext = ydoc.getText('document');

// Array - for lists
const yarray = ydoc.getArray('items');

// Map - for key-value
const ymap = ydoc.getMap('settings');

// Object - nested data
const yobj = ydoc.getObject('user');

// XML Fragment - rich text
const yxml = ydoc.getXmlFragment('content');

Basic Operations

// Text operations
const ytext = ydoc.getText('doc');

// Insert
ytext.insert(0, 'Hello World');
ytext.delete(0, 5); // Delete 5 chars from position 0

// Get content
console.log(ytext.toString());

// Get length
console.log(ytext.length);

Array Operations

const yarray = ydoc.getArray('items');

// Push to end
yarray.push(['item1', 'item2']);

// Insert at position
yarray.insert(0, ['newItem']);

// Delete
yarray.delete(0, 1); // Delete 1 item at index 0

// Observe
yarray.observe(event => {
  event.changes.delta.forEach(change => {
    if (change.insert) console.log('Inserted:', change.insert);
    if (change.delete) console.log('Deleted:', change.delete);
  });
});

Map Operations

const ymap = ydoc.getMap('settings');

// Set values
ymap.set('theme', 'dark');
ymap.set('notifications', true);

// Get value
console.log(ymap.get('theme'));

// Delete
ymap.delete('notifications');

// Observe
ymap.observe(event => {
  event.keys.forEach(key => {
    console.log(`${key}: ${ymap.get(key)}`);
  });
});

Real-Time Collaboration

WebSocket Provider

// Server using y-websocket
import { WebSocketServer } from 'ws';
import { setupWSConnection } from 'y-websocket/bin/utils';

const wss = new WebSocketServer({ port: 1234 });

wss.on('connection', (ws, req) => {
  setupWSConnection(ws, req);
});
// Client
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

const ydoc = new Y.Doc();
const ytext = ydoc.getText('document');

// Connect to WebSocket server
const provider = new WebsocketProvider(
  'ws://localhost:1234',
  'my-room',
  ydoc
);

// Observe connection status
provider.on('status', event => {
  console.log('Connection:', event.status);
});

// The document syncs automatically!
ytext.insert(0, 'Hello from client!');

Awareness (Presence)

// Share cursor position and user info
const awareness = provider.awareness;

// Set local user info
awareness.setLocalState({
  user: {
    name: 'John',
    color: '#ff9900',
  },
  cursor: {
    index: 10,
    length: 0,
  },
});

// Observe others
awareness.on('change', () => {
  const states = awareness.getStates();
  
  states.forEach((state, clientId) => {
    if (clientId !== awareness.clientID) {
      console.log('User:', state.user);
    }
  });
});

Integration Examples

React

// useYjs hook
import { useState, useEffect, createContext, useContext } from 'react';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

const YjsContext = createContext(null);

export function YjsProvider({ children, room }) {
  const [doc] = useState(() => new Y.Doc());
  const [provider, setProvider] = useState(null);

  useEffect(() => {
    const wsProvider = new WebsocketProvider(
      'ws://localhost:1234',
      room,
      doc
    );
    setProvider(wsProvider);

    return () => wsProvider.destroy();
  }, [room, doc]);

  return (
    <YjsContext.Provider value={{ doc, provider }}>
      {children}
    </YjsContext.Provider>
  );
}

// Component using shared text
export function Editor() {
  const { doc } = useContext(YjsContext);
  const [text, setText] = useState('');

  useEffect(() => {
    const ytext = doc.getText('content');
    
    const observer = () => {
      setText(ytext.toString());
    };
    
    ytext.observe(observer);
    setText(ytext.toString());
    
    return () => ytext.unobserve(observer);
  }, [doc]);

  return (
    <textarea
      value={text}
      onChange={e => {
        const ytext = doc.getText('content');
        ytext.delete(0, ytext.length);
        ytext.insert(0, e.target.value);
      }}
    />
  );
}

Quill Editor

// Yjs with Quill
import Quill from 'quill';
import * as Y from 'yjs';
import { QuillBinding } from 'y-quill';

const ydoc = new Y.Doc();
const ytext = ydoc.getText('quill');

const editor = new Quill('#editor', {
  modules: { toolbar: true },
});

// Bind Yjs to Quill
const binding = new QuillBinding(ytext, editor, provider.awareness);

Monaco Editor

// Yjs with Monaco
import * as Y from 'yjs';
import { MonacoBinding } from 'y-monaco';
import * as monaco from 'monaco-editor';

const editor = monaco.editor.create(document.getElementById('editor'), {
  value: '',
  language: 'javascript',
});

const ydoc = new Y.Doc();
const ytext = ydoc.getText('code');

const binding = new MonacoBinding(
  ytext,
  editor.getModel(),
  new Set([editor]),
  provider.awareness
);

Persistence

IndexedDB (Browser)

import { IndexeddbPersistence } from 'y-indexeddb';

const persistence = new IndexeddbPersistence('my-document', ydoc);

persistence.on('synced', () => {
  console.log('Content loaded from IndexedDB');
});

LevelDB (Node.js)

import { LeveldbPersistence } from 'y-leveldb';

const persistence = new LeveldbPersistence('./data/leveldb', ydoc);

persistence.on('synced', () => {
  console.log('Content loaded from LevelDB');
});

SQLite

import { SqljsPersistence } from 'y-sqljs';

const sqlPromise = initSqlJs({
  locateFile: file => `https://sql.js.org/dist/${file}`
});

const persistence = new SqljsPersistence(
  sqlPromise,
  ydoc,
  { table: 'documents' }
);

HTTP/WebDAV

import { HttpPersistence } from 'y-http';

const persistence = new HttpPersistence(
  'https://your-server.com/api/documents',
  ydoc
);

Conflict Resolution

Understanding Merging

// Automatic merge example
const ydoc1 = new Y.Doc();
const ydoc2 = new Y.Doc();

// User 1 edits
const text1 = ydoc1.getText('doc');
text1.insert(0, 'Hello');

// User 2 edits simultaneously
const text2 = ydoc2.getText('doc');
text2.insert(0, 'Hi ');

// Merge - both get both changes!
Y.applyUpdate(ydoc1, Y.encodeStateAsUpdate(ydoc2));
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc1));

console.log(ydoc1.getText('doc').toString()); // "Hi Hello"
console.log(ydoc2.getText('doc').toString()); // "Hi Hello"

Custom Merge

// Sometimes you need custom logic
ymap.observe(event => {
  event.changes.keys.forEach((change, key) => {
    if (change.action === 'update') {
      // Custom conflict resolution
      const local = change.value;
      const remote = fetchRemoteValue(key);
      
      // Use "last write wins" or merge
      if (remote.timestamp > local.timestamp) {
        ymap.set(key, remote.value);
      }
    }
  });
});

Performance Tips

1. Batching

// Batch multiple changes
ydoc.transact(() => {
  for (let i = 0; i < 1000; i++) {
    yarray.push([i]);
  }
2. Snapshots

```javascript
});

// Save memory with snapshots

const snapshot = Y.snapshot(ydoc);

// Later, restore Y.applySnapshot(ydoc, snapshot);


### 3. Encoding Updates

```javascript
// Only sync what's changed
const update = Y.encodeStateAsUpdate(ydoc);
console.log('Update size:', update.byteLength);

// Apply to other clients
Y.applyUpdate(otherDoc, update);

Use Cases

Collaborative Text Editor

// Build Google Docs in ~100 lines
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
import { QuillBinding } from 'y-quill';

// 1. Create doc
const ydoc = new Y.Doc();

// 2. Connect
const provider = new WebsocketProvider(
  'ws://localhost:1234',
  'document-room',
  ydoc
);

// 3. Create editor
const editor = new Quill('#editor', { theme: 'snow' });

// 4. Bind!
const binding = new QuillBinding(
  ydoc.getText('content'),
  editor,
  provider.awareness
);

// Done! Real-time collaboration works!

Collaborative Drawing

// Yjs for drawing
const ymap = ydoc.getMap('shapes');

function addShape(shape) {
  ymap.set(shape.id, shape);
}

function updateShape(id, changes) {
  const shape = ymap.get(id);
  ymap.set(id, { ...shape, ...changes });
}

// Share with others via WebSocket
// Works with any canvas library!

Yjs vs Alternatives

Feature Yjs Automerge Riak
Type Support All CRDTs Limited Limited
Performance Fast Moderate Slow
Size Small Large Large
Framework Support Many Few None
Active Dev โœ… โœ… โŒ

Conclusion

Yjs makes real-time collaboration achievable:

  • Simple API - just use shared types
  • Automatic merging - no conflicts
  • Framework agnostic - works everywhere
  • Rich ecosystem - editors, providers
  • Performant - handles millions of operations

Perfect for: Editors, drawings, games, real-time apps.


External Resources

Comments