js

How to Build Real-Time Collaborative Apps with Yjs and WebSockets

Learn how to create scalable, real-time collaborative features in React using Yjs CRDTs and WebSockets. Start building today.

How to Build Real-Time Collaborative Apps with Yjs and WebSockets

I’ve been thinking about collaboration a lot lately. Not just the idea of working together, but the actual mechanics of it. How do multiple people edit the same document at the same time without creating chaos? How does a cursor from another user appear on my screen as they type? This question led me down a path, exploring the tools that make modern apps like Google Docs or Figma possible. Today, I want to share what I learned about building these features yourself.

The core challenge is synchronization. When two people type in the same place, who wins? Traditional methods often involve complex server logic to merge changes. But there’s a better way. It’s called a Conflict-free Replicated Data Type, or CRDT. Think of it as a data structure designed from the ground up to be edited in multiple places at once. It guarantees that all copies will eventually look the same, no matter the order changes arrive.

This is where Yjs comes in. It’s a library that implements CRDTs for JavaScript. Instead of sending entire documents back and forth, Yjs manages small, mergeable updates. It handles the hard math of consistency. Your job is to connect it to your app and a network. For the network, we often use WebSockets—a protocol for persistent, two-way communication between a browser and a server.

Why does this combination work so well? Yjs manages the data, and WebSockets provide the real-time pipe to share it. The server doesn’t need to understand the document’s content; it just passes messages. This separation makes the system robust and scalable.

Let’s start building. First, we need a WebSocket server that speaks Yjs’s language. You can set one up with Node.js. The y-websocket package provides utilities to make this straightforward. Here’s a basic server:

const WebSocket = require('ws');
const http = require('http');
const { setupWSConnection } = require('y-websocket/bin/utils');

const server = http.createServer();
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws, request) => {
  // Extract the document name from the URL, like 'ws://server/my-doc'
  const docName = request.url.slice(1);
  setupWSConnection(ws, request, { docName });
});

server.listen(4444, () => {
  console.log('Yjs WebSocket server running on port 4444');
});

This server creates a “room” for each unique document name. All clients connecting to the same room will share updates. Notice the server doesn’t store the document. It just relays messages. The clients hold the true state.

Now, for the React application. We need to connect to this server and create a shared Yjs document. We’ll wrap this logic in a custom hook for reusability. The hook will manage the connection and provide the document state to our components.

import { useEffect, useRef, useState } from 'react';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';

function useCollaborativeDoc(roomName) {
  const [isConnected, setIsConnected] = useState(false);
  const docRef = useRef(null);
  const providerRef = useRef(null);

  useEffect(() => {
    // 1. Create the Yjs document
    const ydoc = new Y.Doc();
    docRef.current = ydoc;

    // 2. Connect to the WebSocket server
    const provider = new WebsocketProvider(
      'ws://localhost:4444',
      roomName,
      ydoc
    );
    providerRef.current = provider;

    provider.on('status', event => {
      setIsConnected(event.status === 'connected');
    });

    // Cleanup on unmount
    return () => {
      provider.disconnect();
      ydoc.destroy();
    };
  }, [roomName]);

  return { doc: docRef.current, isConnected };
}

What’s happening here? We create a Y.Doc object. This is our shared data store. Then, we create a WebsocketProvider. It links the Y.Doc to our server. Any change made to the ydoc locally is automatically encoded and sent to the server, which broadcasts it to others in the room.

But a shared document object isn’t very useful by itself. We need to bind it to something a user can interact with, like a text field. Let’s create a collaborative text area. Yjs stores text in a special shared type called Y.Text.

function CollaborativeTextArea({ roomName }) {
  const { doc } = useCollaborativeDoc(roomName);
  const textAreaRef = useRef(null);

  useEffect(() => {
    if (!doc) return;

    // Get or create the shared text object
    const yText = doc.getText('content');

    // Function to update the textarea when Yjs changes
    const updateTextArea = () => {
      if (textAreaRef.current) {
        textAreaRef.current.value = yText.toString();
      }
    };

    // Observe changes from remote users
    yText.observe(updateTextArea);

    // Set initial value
    updateTextArea();

    // Cleanup observer
    return () => {
      yText.unobserve(updateTextArea);
    };
  }, [doc]);

  const handleChange = (event) => {
    if (!doc) return;
    const yText = doc.getText('content');
    const newValue = event.target.value;

    // To update Yjs, we need to calculate the difference.
    // A simple way: replace the entire text.
    // In a real app, you'd use more efficient edits.
    doc.transact(() => {
      yText.delete(0, yText.length);
      yText.insert(0, newValue);
    });
  };

  return <textarea ref={textAreaRef} onChange={handleChange} />;
}

This component syncs a <textarea> with a Y.Text object. The observe method lets us react when other users modify the text. Our handleChange function modifies the shared yText object. The transact() method groups changes into a single update that gets synchronized.

Have you wondered how apps show you where other users are? That’s called “awareness.” It’s information about presence—like cursor positions, selected text, or just a name. Yjs has a built-in awareness protocol. Let’s add a simple cursor position share.

We need to update our provider to enable awareness and listen for changes.

// In the useCollaborativeDoc hook, update the provider setup:
const provider = new WebsocketProvider(
  'ws://localhost:4444',
  roomName,
  ydoc,
  { connect: true, awareness: true } // Enable awareness
);

// We can now set local state, like a cursor position
provider.awareness.setLocalState({
  user: { name: 'You', color: '#FF6B6B' },
  cursor: { pos: 0 }
});

// And listen for other users' states
provider.awareness.on('change', () => {
  const states = provider.awareness.getStates();
  console.log('All users:', Array.from(states.values()));
});

In your component, you could listen to cursor movements and update the awareness state. Other clients would receive this update and could render a little avatar or colored cursor at that position.

What happens when the internet drops? One of the best things about CRDTs is their handling of offline work. Yjs stores all changes locally. When you reconnect, the WebsocketProvider automatically syncs the missed changes with the server. The CRDT math ensures everything merges correctly. The user might not even notice they were offline.

This is just the start. You can share more than text. Yjs has shared types for maps (Y.Map), arrays (Y.Array), and even custom data. You could build a collaborative to-do list, a shared whiteboard, or a live form editor. The pattern is similar: create a shared type, observe it, and update it based on user actions.

Performance is a common concern. Yjs is efficient. It uses a compact binary format for updates. For very large documents, you can structure data into separate shared types or even multiple Yjs documents. The library is built to handle this scale.

So, where do you begin? Start simple. Set up the server and get two browser windows talking to each other. Make a shared text box work. Then add awareness. Then try a different data type, like a shared list of items. Each step builds your understanding.

The magic of real-time collaboration isn’t really magic. It’s the result of clever data structures and reliable networking. With Yjs and WebSockets, you have the foundation to add these powerful features to your own React applications. You can move beyond simple static pages and create dynamic, multi-user experiences.

What kind of collaborative app have you always wanted to build? Could these tools help you prototype it? I’d love to hear your ideas or answer any questions you have. If you found this walkthrough helpful, please share it with others who might be curious about building together. Let me know in the comments what you create


As a best-selling author, I invite you to explore my books on Amazon. Don’t forget to follow me on Medium and show your support. Thank you! Your support means the world!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!


📘 Checkout my latest ebook for free on my channel!
Be sure to like, share, comment, and subscribe to the channel!


Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Keywords: real-time collaboration,Yjs,WebSockets,React,CRDT



Similar Posts
Blog Image
Build Event-Driven Architecture with Redis Streams and Node.js: Complete Implementation Guide

Master event-driven architecture with Redis Streams & Node.js. Learn producers, consumers, error handling, monitoring & scaling. Complete tutorial with code examples.

Blog Image
How to Scale Socket.IO with Redis: Complete Guide for Real-Time Application Performance

Learn how to integrate Socket.IO with Redis for scalable real-time apps. Build chat systems, dashboards & collaborative tools that handle thousands of connections seamlessly.

Blog Image
How to Build Full-Stack TypeScript Apps: Complete Next.js and Prisma ORM Integration Guide

Learn how to integrate Next.js with Prisma ORM for type-safe full-stack development. Build modern web apps with seamless database operations and TypeScript support.

Blog Image
How to Integrate Svelte with Firebase: Complete Guide for Real-Time Web Applications

Learn how to integrate Svelte with Firebase for powerful full-stack apps. Build reactive UIs with real-time data, authentication, and seamless deployment.

Blog Image
Complete Guide to Integrating Next.js with Prisma ORM: Build Type-Safe Full-Stack Applications

Learn to integrate Next.js with Prisma ORM for type-safe database operations. Build full-stack apps with seamless data handling and TypeScript support.

Blog Image
Complete Guide to Building Rate-Limited GraphQL APIs with Apollo Server, Redis and TypeScript

Learn to build a production-ready GraphQL API with Apollo Server, TypeScript & Redis. Master rate limiting strategies, custom directives & deployment. Complete tutorial with code examples.