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