
When Your UI Needs to Be Truly Collaborative: Yjs for Real-Time Shared State
Building a real-time collaborative application is notoriously difficult. Yjs offers a pragmatic solution by providing shared data types and a robust synchronization engine, enabling multiple users to edit the same document without complex conflict resolution logic.
by Sunil Band
The Hard Problem of Real-Time Collaboration
We've all used Google Docs or Figma, where multiple cursors dance around the screen, and edits appear instantly. It feels magical, but achieving that seamless experience in your own applications is a beast. The moment you introduce more than one client editing the same data, you're not just dealing with state management; you're wrestling with distributed systems. How do you ensure everyone sees a consistent view? What happens when two users edit the same line of text concurrently? Most applications punt on this, falling back to "last write wins" or locking mechanisms, which are terrible for user experience.
The core challenge lies in conflict resolution. Traditional state management, even with immutability, assumes a single source of truth or a controlled update flow. In a collaborative environment, every client is a potential source of truth. You need a way to merge divergent states intelligently, often at a granular level, without losing data or forcing users to wait. This is where Conflict-free Replicated Data Types (CRDTs) come into play, and Yjs is a fantastic, production-ready implementation of this concept.
Yjs: Shared Data Types, Not Just State
Yjs isn't just a state management library; it's a framework for collaborative data structures. Instead of telling you how to sync your existing application state, it provides its own data types that are inherently collaborative. This is a crucial distinction. When you use a Y.Text or Y.Map, you're not just storing data; you're storing data that knows how to resolve conflicts and propagate changes across multiple clients automatically. It's like having a database that lives in every browser, constantly synchronizing with its peers.
This approach drastically simplifies development. You don't write custom diffing or merging logic. You operate on Yjs data types just like you would on native JavaScript objects, and Yjs handles the hard parts of ensuring eventual consistency and conflict resolution. It's optimized for efficiency, sending only the diffs, not the whole document, and it's framework agnostic. You can integrate it with React, Vue, Svelte, or vanilla JavaScript.
Getting Started with Yjs
Let's build a simple collaborative text editor. We'll need a Yjs document, a Y.Text type to hold our content, and a provider to handle the actual network synchronization. For this example, we'll use y-websocket, which connects clients to a WebSocket server. You'd typically run a small y-websocket server alongside your application, but for quick testing, there are public instances you can use.
First, install the necessary packages:
npm install yjs y-websocketNow, let's set up a basic React component. We'll use a textarea for editing and connect it to our Y.Text instance.
import React, { useEffect, useRef, useState, useCallback } from 'react';
import * as Y from 'yjs';
import { WebsocketProvider } from 'y-websocket';
// A public test WebSocket server. For production, you'd run your own.
const WEBSOCKET_URL = 'ws://localhost:1234'; // Or 'wss://demos.yjs.dev/demos/websocket/YOUR-ROOM-NAME'
const DOC_NAME = 'my-collaborative-document';
function CollaborativeEditor() {
const ydocRef = useRef<Y.Doc | null>(null);
const yTextRef = useRef<Y.Text | null>(null);
const providerRef = useRef<WebsocketProvider | null>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null);
// Using useState to re-render when text changes, though Yjs also supports direct DOM manipulation.
const [text, setText] = useState('');
useEffect(() => {
// Initialize Y.Doc and Y.Text only once
if (!ydocRef.current) {
const ydoc = new Y.Doc();
const yText = ydoc.getText(DOC_NAME); // Get a Y.Text type by name
ydocRef.current = ydoc;
yTextRef.current = yText;
// Connect to the WebSocket provider
const provider = new WebsocketProvider(
WEBSOCKET_URL,
DOC_NAME, // Room name for collaboration
ydoc,
{ connect: true }
);
providerRef.current = provider;
// Listen for changes on the Y.Text and update React state
yText.observe(event => {
// Yjs sends fine-grained events, but for a textarea, we often just grab the whole text
setText(yText.toString());
});
// Initialize the textarea with current content
setText(yText.toString());
}
return () => {
// Clean up provider connection on unmount
if (providerRef.current) {
providerRef.current.destroy();
}
};
}, []);
// Handle changes from the textarea and apply them to Y.Text
const handleChange = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (yTextRef.current) {
const newText = e.target.value;
// Update Y.Text. Yjs handles the diffing and synchronization.
// For a simple textarea, we replace the whole content.
// For richer editors, you'd use insert/delete at specific positions.
yTextRef.current.delete(0, yTextRef.current.length);
yTextRef.current.insert(0, newText);
}
}, []);
return (
<div>
<h1>Collaborative Document</h1>
<textarea
ref={textareaRef}
value={text}
onChange={handleChange}
rows={10}
cols={80}
placeholder="Start typing here..."
/>
<p>Open this page in multiple tabs or browsers to see real-time collaboration!</p>
</div>
);
}
export default CollaborativeEditor;This simple example demonstrates the core idea: you get a Y.Doc, retrieve a collaborative type like Y.Text from it, and then bind your UI to that type. When you update the Y.Text (e.g., in handleChange), Yjs automatically serializes the changes and sends them via the WebsocketProvider to all other connected clients. When other clients receive updates, yText.observe fires, and your UI updates. Crucially, if two users type at the same position simultaneously, Yjs (using its underlying CRDT logic) will merge those changes deterministically without corruption.
Deeper Dive into Yjs Types
Y.Text is great for plain text, but Yjs offers more:
-
Y.Map: For collaborative objects (key-value pairs). Think of a shared JSON object where users can add, remove, or modify properties. -
Y.Array: For collaborative lists. Users can insert, delete, or move items, and Yjs maintains order and consistency. -
Y.XmlFragment/Y.XmlElement: For collaborative rich text or structured content. This is what powers complex editors like ProseMirror or TipTap when integrated with Yjs, allowing for collaborative editing of paragraphs, formatting, embedded elements, etc.
Each of these types exposes methods similar to their JavaScript counterparts (set, get, delete, insert, splice), but these operations are now collaborative and conflict-free.
The Trade-offs and Considerations
Yjs is powerful, but it's not a silver bullet. Here are a few things to keep in mind:
- Complexity under the hood: While Yjs simplifies your code, the underlying CRDT algorithms are complex. Debugging subtle synchronization issues can be challenging, though
y-websocketis quite robust. - Server-side persistence: Yjs focuses on real-time synchronization between clients. If you need to persist your document state server-side (which you almost certainly do for any serious application), you'll need to save the
Y.Docstate periodically. Yjs providesY.encodeStateAsUpdateandY.applyUpdatefor this, allowing you to save and load binary document states. - Data model shift: You might need to adjust your application's data model to leverage Yjs's collaborative types effectively. Instead of plain objects or arrays, you'll be working with
Y.Map,Y.Array, etc. This usually means a small wrapper or adapter layer if you're integrating with an existing codebase. - Operational overhead: Running a WebSocket server for
y-websocket(or a more advanced provider likey-leveldbfor persistent storage) adds a component to your infrastructure. However,y-websocketis quite lightweight and can run alongside your existing backend services.
Despite these, the benefits of automatic conflict resolution and a robust, battle-tested synchronization engine often outweigh the setup cost. Yjs frees you from reinventing a wheel that's incredibly difficult to get right.
Wrapping up
If you're building any application where multiple users need to interact with the same live data – whether it's a document editor, a whiteboard, a collaborative form, or even a real-time analytics dashboard – Yjs is a tool you need to seriously consider. It fundamentally changes how you approach shared state, moving the heavy lifting of synchronization and conflict resolution into a well-engineered library.
My advice: Clone the y-websocket server (it's a small Node.js project) and run it locally. Then, adapt the CollaborativeEditor component I showed above into your own React project. Open two browser tabs to localhost:3000 (or wherever your app runs) and start typing. Watch the magic happen. You'll quickly see how powerful it is to have truly shared, conflict-free data types at your disposal.

When Your Database Needs to Think: Adding AI Search with pgvector
Semantic search using embeddings is a game-changer, but integrating it often feels like a separate service problem. What if your existing Postgres database could handle it natively?

When Your Data Visualization Needs to Be More Than Just a Static Chart: Diving into Plotly.js
Tired of static charts that only tell half the story? Plotly.js offers a powerful way to bring your data to life with rich interactivity, letting users explore and understand complex datasets directly in their browser. It's not just about pretty graphs; it's about making data explorable.

When Your Markdown Notes Need to Act Like a Database: Diving into SilverBullet
I've been using Markdown for notes and documentation for years, but it always felt like I was leaving so much on the table. How do you query across files? How do you create dynamic dashboards? SilverBullet finally tackles this, turning simple Markdown into a surprisingly powerful, queryable knowledg


















