@lexical/yjs
@lexical/yjs is the CRDT binding between a Lexical editor and a Yjs document. It translates Lexical node and selection changes into Yjs shared data, applies remote Yjs updates back to the editor, tracks collaborator awareness and cursor positions, and creates a Yjs-aware undo manager. Most React applications consume it indirectly through `CollaborationPlugin` from @lexical/react; it does not provide networking, a collaboration server, persistence, authentication, or access control.
The right binding when the editor is Lexical and the collaboration model is Yjs, with first-party React integration and active releases. Budget for the provider, persistence, security, bootstrapping, version alignment, and custom-node audits; this package handles editor synchronization, not the collaboration product around it.
Use it if
- You are building real-time collaborative editing on Lexical and have chosen Yjs as the shared document model
- You want the official binding used by Lexical's React CollaborationPlugin and playground
- You need collaborator cursors, awareness state, connection commands, and Yjs-scoped undo history
- You can own the provider, room naming, server persistence, authentication, and initial document lifecycle
- You want collaboration by installing one package: the Lexical guide requires Lexical, @lexical/react, Yjs, a provider such as y-websocket, and a running synchronization service
- You cannot keep all Lexical packages on the same release: @lexical/yjs 0.49.0 depends exactly on lexical, @lexical/internal, and @lexical/selection 0.49.0, with Yjs 13.5.22 or later as a peer
- You plan to initialize production content independently in each browser: the official guide warns that simultaneous client bootstrapping can corrupt the document and recommends server-side initialization
- Your custom nodes leave optional synchronized properties unassigned: the collaboration guide says synced custom properties must exist as own enumerable properties, usually by assigning them in the constructor
- You require a settled 1.0 API: the package is still 0.x and exposes explicitly experimental V2 binding, sync, and version-diff APIs that may change
Setup reality
The minimal React install from the official guide is npm install @lexical/react @lexical/yjs lexical react react-dom y-websocket yjs. TypeScript users need 5.2 or later, Yjs must be at least 13.5.22, and the Lexical packages should all use exactly 0.49.0 because this package pins its Lexical dependencies to that version. There are no native builds or config files in the binding itself, but a working system needs much more: a Yjs provider, a WebSocket or other transport service, stable room IDs, authentication, authorization, persistence, retry and offline policy, monitoring, and cleanup. The Lexical docs say y-websocket is the only officially supported provider, though compatible providers may work. In the editor config set `editorState: null`; otherwise Lexical may create its own initial state before collaboration takes control. A provider factory must create or retrieve the Y.Doc in the supplied map and should use `connect: false`, because CollaborationPlugin manages connection and disconnection. In production, initialize the shared document on the server and pass `shouldBootstrap={false}`. The docs reserve `shouldBootstrap={true}` plus an initial editor state for local testing because two clients bootstrapping at once can corrupt content. Awareness is ephemeral presence, not durable user data. Custom node properties sync only when they are own enumerable properties assigned during construction, unless you use NodeState. The binding does not secure room names or provider messages. Your server must decide who may join and what document each ID maps to. For non-React use, the exported binding and sync primitives are low level; you must wire editor updates, Yjs observers, awareness, undo, teardown, and errors yourself, so study the React implementation before reproducing it.
Patterns
Install the documented React collaboration stackinstall-react-stack
npm install @lexical/react@0.49.0 @lexical/yjs@0.49.0 lexical@0.49.0 react react-dom y-websocket yjsKeep Lexical packages aligned. @lexical/yjs 0.49.0 requires Yjs 13.5.22 or later and TypeScript 5.2 or later when using types.
Create a memoized y-websocket provider factorycreate-provider-factory
import {useCallback} from 'react';
import * as Y from 'yjs';
import {WebsocketProvider} from 'y-websocket';
const providerFactory = useCallback((id: string, docs: Map<string, Y.Doc>) => {
let doc = docs.get(id);
if (!doc) {
doc = new Y.Doc();
docs.set(id, doc);
}
return new WebsocketProvider('wss://collab.example.com', id, doc, {connect: false});
}, []);Use an authenticated server and authorize the room ID. connect false lets CollaborationPlugin control connection lifecycle.
Let collaboration own initial editor stateconfigure-collab-editor
const initialConfig = {
namespace: 'DocumentEditor',
editorState: null,
nodes: [MyNode],
onError(error: Error) { throw error; },
theme: {},
};editorState must be null so Lexical does not initialize competing local content before the Yjs document loads.
Mount the React collaboration integrationmount-collaboration-plugin
import {LexicalCollaboration} from '@lexical/react/LexicalCollaborationContext';
import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
<LexicalCollaboration>
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin contentEditable={<ContentEditable />} ErrorBoundary={LexicalErrorBoundary} />
<CollaborationPlugin
id={documentId}
providerFactory={providerFactory}
shouldBootstrap={false}
/>
</LexicalComposer>
</LexicalCollaboration>Use shouldBootstrap false for production and initialize the Y.Doc on the server.
Bootstrap content for a local-only demobootstrap-local-demo
<CollaborationPlugin
id="demo-room"
providerFactory={providerFactory}
initialEditorState={$initialEditorState}
shouldBootstrap={true}
/>The official guide limits client bootstrapping to development; simultaneous production clients can both initialize and corrupt content.
Observe provider connection status through Lexicaltrack-connection-status
import {CONNECTED_COMMAND} from '@lexical/yjs';
import {COMMAND_PRIORITY_LOW} from 'lexical';
const unregister = editor.registerCommand(
CONNECTED_COMMAND,
(connected) => { setConnected(connected); return false; },
COMMAND_PRIORITY_LOW,
);Call the unregister function during component cleanup to avoid accumulating command handlers.
Disconnect and reconnect collaborationtoggle-connection
import {TOGGLE_CONNECT_COMMAND} from '@lexical/yjs';
editor.dispatchCommand(TOGGLE_CONNECT_COMMAND, false); // disconnect
editor.dispatchCommand(TOGGLE_CONNECT_COMMAND, true); // reconnectDisconnecting transport does not authorize edits or erase local Yjs state; enforce access on the provider server.
Attach ephemeral collaborator metadataattach-awareness-data
<CollaborationPlugin
id={documentId}
providerFactory={providerFactory}
shouldBootstrap={false}
username={user.displayName}
cursorColor={user.cursorColor}
awarenessData={{userId: user.id, avatarUrl: user.avatarUrl}}
/>Awareness is broadcast presence, not durable storage or trusted identity. Avoid secrets and verify permissions server-side.
Initialize every synchronized custom node propertysync-custom-node-property
class MentionNode extends TextNode {
__userId: string | undefined;
constructor(text: string, userId?: string, key?: NodeKey) {
super(text, key);
this.__userId = userId;
}
}An optional TypeScript declaration alone may create no own property. Assign it in every constructor path or use NodeState.
Create a V1 binding without Reactcreate-low-level-binding
import {createYjsBinding} from '@lexical/yjs';
import * as Y from 'yjs';
const id = 'document-42';
const doc = new Y.Doc();
const docMap = new Map([[id, doc]]);
const binding = createYjsBinding({editor, id, doc, docMap});This creates binding state only. Non-React callers still must wire Lexical updates, Yjs observers, awareness, undo, connection, and teardown.
Create a Yjs-scoped undo managercreate-collab-undo-manager
import {createUndoManager} from '@lexical/yjs';
const undoManager = createUndoManager(
binding,
binding.root.getSharedType(),
);
undoManager.undo();
undoManager.redo();The React CollaborationPlugin wires collaborative history for you; use this low-level helper only when managing the binding lifecycle yourself.
Use a custom Yjs shared root namecustomize-shared-root
const binding = createYjsBinding({
editor,
id: documentId,
doc,
docMap,
rootName: 'article-body',
});Every peer for a document must use the same rootName or they will edit different shared types.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| y-prosemirror | npm | Your editor is ProseMirror or a ProseMirror-based framework and you want its established Yjs binding |
| @tiptap/extension-collaboration | npm | You use Tiptap and want its packaged Yjs collaboration extension and editor integration |
| y-quill | npm | You need Yjs collaboration for an existing Quill editor |
| @lexical/react | npm | You use React and want the higher-level CollaborationPlugin that manages this binding's lifecycle |