Two people open the same customer brief. One corrects a delivery date while the other rewrites the opening paragraph. If the application saves the entire document after each edit, the second save can erase the first person’s work. Adding a WebSocket makes the updates travel faster; it does not define how they should merge.
For a React application that needs simultaneous text editing, a CRDT library such as Yjs can provide the merge behavior. You still need an editor binding, transport, storage, authorization and a recovery plan. Those decisions determine whether the feature remains reliable after the two-tab demonstration.
This guide explains those boundaries, includes a small executable merge example and proposes a production evaluation checklist. It is intended for teams adding collaboration to a SaaS product, where the editor is part of a larger application with existing users, permissions and data.
Establish whether you need concurrent editing
Start with the workflow. A report that only one person edits at a time may work well with a lock or a version check. An approval form may need explicit conflict resolution because merging two decisions automatically would be inappropriate. A planning document where several colleagues type together has a different requirement.
Optimistic concurrency is often sufficient for ordinary records. The client submits the version it read, and the server rejects a write if the record changed in the meantime. The user can review the conflict. This preserves a clear business decision without requiring a collaborative text model.
CRDTs become more attractive when people need to edit the same content concurrently, when local changes should remain responsive during network interruptions, or when reconnecting clients must merge changes made independently.
Notion’s account of its CRDT implementation explains why its earlier last-write-wins behavior became inadequate, particularly with offline editing. It is also a reminder that rich text involves more than merging strings: formatting and block operations introduce their own rules. Your product may need a much smaller subset of that behavior.
What Yjs supplies, and what the application must supply
Yjs provides shared data types and a document model for synchronizing changes. For text, operations refer to a shared structure rather than sending a complete replacement string on every keystroke.
In an application, separate these responsibilities:
Layer | Responsibility | A question to resolve |
|---|---|---|
React interface | Document navigation, controls and connection status | What happens when the user switches documents? |
Editor and binding | Selection, rich-text behavior and translation to shared operations | Does the editor support the schema we need? |
Yjs document | Shared content and merge behavior | What is the boundary of one document? |
Provider | Exchange updates with other clients | Who accepts and distributes updates? |
Persistence | Recover durable content after restart | What does “saved” mean to the user? |
Application backend | Identity, access rules and business records | Who can read or change this document? |
Keep application records such as billing status, workspace membership and approval decisions in the system that already owns their rules. They do not all become collaborative data merely because the document editor uses a CRDT.
The same distinction applies to existing reactive backends. A subscription that refreshes query results can keep a dashboard current. It does not automatically give a rich-text field character-level merge semantics. Your real-time React architecture and your editor’s collaboration model may solve different problems.
A minimal example of independent edits converging
The following example uses two Yjs documents in one process. There is no network, React component or database. Its purpose is to make the merge behavior visible before adding infrastructure.
Install Yjs 13.6.33 in a small test project, save this as merge.mjs and run it with Node:
import assert from 'node:assert/strict';
import * as Y from 'yjs';
const first = new Y.Doc();
const second = new Y.Doc();
first.getText('brief').insert(0, 'Delivery Friday.');
Y.applyUpdate(second, Y.encodeStateAsUpdate(first));
first.getText('brief').insert(0, 'Confirmed: ');
const secondText = second.getText('brief');
secondText.insert(secondText.length, ' Bring ID.');
const firstUpdate = Y.encodeStateAsUpdate(first);
const secondUpdate = Y.encodeStateAsUpdate(second);
Y.applyUpdate(first, secondUpdate);
Y.applyUpdate(second, firstUpdate);
Y.applyUpdate(second, firstUpdate);
assert.equal(first.getText('brief').toString(), secondText.toString());
assert.equal(secondText.toString(), 'Confirmed: Delivery Friday. Bring ID.');
console.log(secondText.toString());
first.destroy();
second.destroy();
Both clients begin with the same shared history. They edit independently and then exchange updates. Reapplying the same update does not append the text again. Yjs documents this property, and the ability to exchange only missing changes using state vectors, in its document update API .
Convergence means the replicas agree after receiving the relevant changes. It does not mean the resulting sentence expresses everyone’s intention. Two people can make individually sensible edits that combine into an awkward paragraph. Product features such as revision history and visible collaborators still matter.
Also notice that the second document receives the first document’s initial state. Do not independently seed the same default paragraph on every client; those would be separate insertions that could both survive synchronization. Give initialization a single owner.
Connect the editor without duplicating its content in React state
Choose an editor with a maintained Yjs integration, such as an appropriate Tiptap, Lexical or other supported binding. Evaluate the specific binding alongside the editor: selection handling, undo behavior, pasted content and composition input are part of the experience.
Let the editor binding translate editing operations into the shared document. Avoid storing a second authoritative copy of the whole document in React state and replacing the editor content whenever a remote update arrives. That approach can interfere with selections and undo, and it hides the operations the collaboration layer is designed to merge.
Create a collaboration session around a stable document identifier. When the route changes to another document, dispose of the previous editor binding, subscriptions and provider. In development, React’s effect lifecycle can expose missing cleanup through duplicate connections or listeners. Treat those symptoms as a lifecycle bug to fix.
For server-rendered applications, initialize browser-dependent editor and persistence code on the client. A read-only server-rendered preview can use a separately maintained content projection. Decide how fresh that preview needs to be and how it is generated; it should not accidentally overwrite the collaborative source.
Keep presence separate from durable content
Cursor positions, selected ranges and “currently typing” indicators have a different lifetime from the document. They should disappear when a participant leaves rather than becoming part of its permanent revision history.
Yjs uses an awareness protocol for this kind of ephemeral state. Editor bindings can use it to display remote selections and participant information.
Choose how much identity to reveal in shared documents. An internal workspace may show full names; an externally shared document may need a different display policy. Do not treat a client-supplied display name as authenticated identity when recording sensitive actions.
Keep presence updates lightweight. A busy room should not persist a database record for every cursor movement. Monitor both content traffic and presence traffic so a feature such as live pointers does not become an unexpected source of load.
Design authorization around the document boundary
A room name is an address, not proof of access. The server must map the authenticated user to the requested tenant and document before returning content or accepting updates. A hidden toolbar cannot enforce read-only access.
The Yjs WebSocket provider offers a conventional client-server transport that can be integrated with authentication. Deploying a provider still requires application-specific access checks and operational configuration.
Plan for permission changes during an open session. Removing a user from a workspace should affect an already connected client, not just their next login. Decide how the server disconnects or restricts that session and how the interface explains the change.
Document boundaries should reflect access boundaries. If a user may read only one private section, sending the entire shared document and hiding the rest in React is insufficient. Split the data or use an architecture that can enforce the required granularity. Offline copies also require a policy: removing server access cannot erase content a user already downloaded.
Define persistence, offline behavior and recovery together
The durable representation should preserve the collaborative history required by your chosen implementation. Saving only an HTML string and reconstructing a fresh Yjs document on every load loses that continuity. HTML or JSON can be useful exports and search projections without being the only stored representation.
Specify when the interface may say “saved.” A local edit, an update accepted by the sync server and an update durably persisted are different events. A connection indicator alone cannot prove that a server restart will preserve the latest change.
For local persistence, y-indexeddb can retain Yjs data in the browser. A complete offline feature additionally needs the application shell, relevant assets and a clear account-switching policy. Do not promise offline editing solely because the document library supports merging disconnected changes.
Test a server restart while clients are active. Restore a document into a fresh browser profile. Reconnect a client that has been offline since before a schema update. Decide how old clients are rejected or upgraded when their editor can no longer understand the current document structure.
Backups and user-facing history solve different problems. A backup restores service data after an incident. Revision history lets a person recover a paragraph they deleted yesterday. Define both before the product becomes dependent on the editor.
Compare managed collaboration with operating your own service
A managed service can reduce the work of maintaining connections, distributing updates and operating persistence. Verify its current guarantees rather than assuming every provider handles offline mode, history or authorization identically.
For a self-hosted implementation, budget for connection routing, resource limits, monitoring, upgrades and recovery drills. A small initial audience does not eliminate those responsibilities. One unusually large or heavily edited document can be more demanding than many quiet rooms.
Use a representative document when comparing options. Include your custom nodes, realistic text length, comments, permissions and participant count. Test the export path as well as the editing path; switching providers later is easier if you understand how to retrieve durable content and metadata.
Price the service using your workload. Relevant measures can include concurrent connections, active users, stored documents, update volume and history retention. A published entry price tells you little about the cost of a large customer with many simultaneous editors.
A useful acceptance test for the first release
Before releasing collaboration, run a small matrix of failure and recovery scenarios:
Two users type into the same paragraph, then both reload.
One user edits offline while another continues online, then the first reconnects.
A participant loses write access while their editor remains open.
The server restarts after accepting an update.
A user undoes their own edit after another participant has changed nearby text.
A document opens in two different browser profiles and survives a restore from storage.
Pasted rich text and composition input behave correctly in the supported browsers.
Record what each user sees, what the server accepts and what is stored. A successful merge demo is one piece of this evidence.
For a new SaaS collaboration feature, begin with a single document type and an explicit permissions model. Makers’ Den’s product engineering team can help evaluate the editor, synchronization service and integration with your existing backend before the feature grows into a separate platform to maintain.