iTranslated by AI
Trying out Lexical + Yjs
Introduction
This is an article about when I tried lexical (Text Editor) developed by Facebook and yjs.
lexical
An extensible text editor framework
It has the following features:
-
Reliable
- Composed of editor instances attached to each editable element.
- Each editor state represents the current state and pending state at any given point in time.
-
Accessible
- Follows best practices established by WCAG (Web Content Accessibility Guidelines) and is compatible with screen readers and other assistive technologies.
-
Fast
- UI components, toolbars, rich text features, markdown, etc., can be added as plugins.
-
Cross Platform
- Available as a JavaScript framework / Swift framework for native iOS development.
Lexical's Design

As shown in the diagram above, Lexical's core is designed to be independent of the contenteditable HTML element and the state.
-
Editor instances
- The core part that links the
contenteditableHTML element and allows updating the EditorState.
- The core part that links the
-
Editor States
- The underlying data model representing what should be displayed on the DOM.
- An immutable object.
- Mainly has two parts:
- Lexical Node Tree
- Lexical Selection object
Collaboration
Lexical provides LexicalCollaborationPlugin and useCollaborationContext hooks in @lexical/react. This is implemented based on the yjs bindings provided by @lexical/yjs.

Environment Setup and Preparation
1. devcontainer Environment Setup
1-1. Base Environment
I will use this again to set up the environment.
I will create a new project called lexical-yjs-example using this as a template and proceed.
1-2. React Environment Setup
$ yarn create vite . --template react-swc-ts
? Current directory is not empty. Please choose how to proceed: › - Use arrow-keys. Return to submit.
> Ignore files and continue
-
Modify
scripts>devinpackage.jsonas follows:
vite --host=0.0.0.0 -
Add the following port to
.devcontainer/docker-compose.yml:ports: - "5173:5173" -
Add the following to
.devcontainer/postAttach.sh:yarn yarn dev
Now, start the devcontainer and access localhost:5173. If the usual screen is displayed, you're good to go.
2. Package Installation
yarn add lexical @lexical/react
Implementation
1. Simple Editor Implementation
1-1. Creating Editor.tsx
Create src/Editor.tsx with the following content:
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
const theme = {};
const onError = (error: Error) => {
console.error(error);
};
const Editor = () => {
const initialConfig = {
namespace: "MyEditor",
theme,
onError,
};
return (
<LexicalComposer initialConfig={initialConfig}>
<RichTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>Enter some text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<AutoFocusPlugin />
</LexicalComposer>
);
};
export default Editor;
1-2. Modifying main.tsx
Modify src/main.tsx as follows:
import React from "react";
import ReactDOM from "react-dom/client";
import Editor from "./Editor.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<Editor />
</React.StrictMode>
);
At this point, if you access http://localhost:5173/, you should see a simple editor like the one below.

2. Enabling Collaborative Editing
2-1. Modifying to Start the y-websocket Server
I will make it so that y-websocket starts when the container is launched. First, add the concurrently package to allow running multiple npm commands simultaneously.
yarn add -D concurrently
Next, modify the scripts in package.json.
"scripts": {
"dev": "concurrently \"vite --host=0.0.0.0\" \"npm:server:ws\"",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"server:ws": "HOST=0.0.0.0 PORT=1234 YPERSISTENCE=./yjs-wss-db npx y-websocket"
},
Finally, add the port to .devcontainer/docker-compose.yml.
ports:
- "1234:1234" # Added
- "5173:5173"
Now, when you restart the container, the y-websocket server should also be running.
2-1. Adding the Collaboration Plugin to Lexical
Install the necessary packages.
yarn add @lexical/yjs y-websocket yjs
Modify the src/Editor.tsx created earlier as follows:
import { AutoFocusPlugin } from "@lexical/react/LexicalAutoFocusPlugin";
import { CollaborationPlugin } from "@lexical/react/LexicalCollaborationPlugin";
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { Provider } from "@lexical/yjs";
import { useCallback } from "react";
import { WebsocketProvider } from "y-websocket";
import * as Y from "yjs";
const theme = {};
const onError = (error: Error) => {
console.error(error);
};
// Add the following method
const getDocFromMap = (id: string, yjsDocMap: Map<string, Y.Doc>): Y.Doc => {
let doc = yjsDocMap.get(id);
if (doc === undefined) {
doc = new Y.Doc();
yjsDocMap.set(id, doc);
} else {
doc.load();
}
return doc;
};
const Editor = () => {
const initialConfig = {
editorState: null, // Required to let CollaborationPlugin set EditorState
nodes: [],
namespace: "MyEditor",
theme,
onError,
};
// Add the following method
const providerFactory = useCallback(
(id: string, yjsDocMap: Map<string, Y.Doc>): Provider => {
const doc = getDocFromMap(id, yjsDocMap);
// Force cast since the official documentation doesn't seem to handle casting well either
return new WebsocketProvider(
"ws://localhost:1234",
id,
doc
) as unknown as Provider;
},
[]
);
// Add CollaborationPlugin
return (
<LexicalComposer initialConfig={initialConfig}>
<CollaborationPlugin
id="lexical/collab"
providerFactory={providerFactory}
shouldBootstrap={false}
/>
<RichTextPlugin
contentEditable={<ContentEditable />}
placeholder={<div>Enter some text...</div>}
ErrorBoundary={LexicalErrorBoundary}
/>
<HistoryPlugin />
<AutoFocusPlugin />
</LexicalComposer>
);
};
export default Editor;
Restart the container again and edit the editor area in multiple tabs. If it synchronizes as shown below, you're all set.

Discussion