Skip to main content

React

Lexical provides LexicalCollaborationPlugin and useCollaborationContext hook within @lexical/react to accelerate creation of the collaborative React backed editors. This is on top of the Yjs bindings provided by @lexical/yjs.

tip

Clone Lexical GitHub repo, run npm i && npm run start and open http://localhost:3000/split/?isCollab=true to launch playground in collaborative mode.

Getting started

This guide is based on examples/react-rich example.

Install minimal set of the required dependencies:

$ npm i -S @lexical/react @lexical/yjs lexical react react-dom y-websocket yjs
note

y-websocket is the only officially supported Yjs connection provider at this point. Although other providers may work just fine.

Get WebSocket server running:

This allows different browser windows and different browsers to find each other and sync Lexical state. On top of this YPERSISTENCE allows you to save Yjs documents in between server restarts so clients can simply reconnect and keep editing.

$ HOST=localhost PORT=1234 YPERSISTENCE=./yjs-wss-db npx y-websocket

Get basic collaborative Lexical setup:

import {$getRoot, $createParagraphNode, $createTextNode} from 'lexical';
import {LexicalCollaboration} from '@lexical/react/LexicalCollaborationContext';
import {LexicalComposer} from '@lexical/react/LexicalComposer';
import {ContentEditable} from '@lexical/react/LexicalContentEditable';
import {LexicalErrorBoundary} from '@lexical/react/LexicalErrorBoundary';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {CollaborationPlugin} from '@lexical/react/LexicalCollaborationPlugin';
import * as Y from 'yjs';
import {$initialEditorState} from './initialEditorState';
import {WebsocketProvider} from 'y-websocket';

function Editor() {
const initialConfig = {
// NOTE: This is critical for collaboration plugin to set editor state to null. It
// would indicate that the editor should not try to set any default state
// (not even empty one), and let collaboration plugin do it instead
editorState: null,
namespace: 'Demo',
nodes: [],
onError: (error: Error) => {
throw error;
},
theme: {},
};

const providerFactory = useCallback(
(id: string, yjsDocMap: Map<string, Y.Doc>) => {
const doc = getDocFromMap(id, yjsDocMap);

return new WebsocketProvider('ws://localhost:1234', id, doc, {
connect: false,
});
}, [],
);

return (
<LexicalCollaboration>
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin
contentEditable={<ContentEditable className="editor-input" />}
placeholder={<div className="editor-placeholder">Enter some rich text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<CollaborationPlugin
id="lexical/react-rich-collab"
providerFactory={providerFactory}
/>
</LexicalComposer>
<LexicalCollaboration>
);
}

Initial editor content:

In a production environment, you should bootstrap the editor's initial content on the server. If bootstrapping was left to the client and two clients connected at the same time, they could both try to initialize the content resulting in document corruption.

Using the withHeadlessCollaborationEditor function from the FAQ page, you can create a bootstrapped Y.Doc with the following:

import type {CreateEditorArgs} from 'lexical';

import {$getRoot, $createParagraphNode} from 'lexical';
import {Doc} from 'yjs';

import {withHeadlessCollaborationEditor} from './withHeadlessCollaborationEditor';

function createBootstrappedYDoc(nodes: CreateEditorArgs['nodes']): Doc {
return withHeadlessCollaborationEditor(nodes, (editor) => {
const yDoc = new Doc();
editor.update(() => {
$getRoot().append($createParagraphNode());
}, {discrete: true});
return yDoc;
});
}

If you're simply following the above example to play around in a local dev environment, then you can add the following props to CollaborationPlugin to initialize the editor state client-side:

// Dev-testing only, do not use in real-world cases.
initialEditorState={$initialEditorState}
shouldBootstrap={true}

See it in action

Source code: examples/react-rich-collab

Building collaborative plugins

Lexical Playground features set of the collaboration enabled plugins that integrate with primary document via useCollaborationContext() hook. Notable mentions:

  • CommentPlugin - features use of the separate provider and Yjs room to sync comments.
  • ImageComponent - features use of the LexicalNestedComposer paired with CollaborationPlugin.
  • PollOptionComponent - showcases poll implementation using clientID from Yjs context.
  • StickyPlugin - features use of the LexicalNestedComposer paired with CollaborationPlugin as well as sticky note position real-time sync.
note

While these "playground" plugins aren't production ready - they serve as a great example of collaborative Lexical capabilities as well as provide a good starting point.

Yjs providers

Setting up the communication between clients, managing awareness information, and storing shared data for offline usage is quite a hassle. Providers manage all that for you and are the perfect starting point for your collaborative app.

See Yjs Website for the list of the officially endorsed providers. Although it's not an exhaustive one.