Skip to content

Lexical React Guide

This document provides additional information for developers in using codox to add collaboration and multi-player capability in Lexical under the React framework

Comment Threads

For enhanced asynchronous collaboration, the CodoxCommentPlugin allows users to interact via threaded comments directly within the document. This plugin is inspired by the Lexical playground’s CommentPlugin and has been adapted to integrate directly with both Codox and Lexical frameworks.

To integrate the comment plugin into your Lexical application, include it within the LexicalComposer component like so:

import {
CodoxCollabPlugin,
CodoxCommentPlugin,
} from '@codoxhq/lexcial-provider';
// init both plugins along with other plugins
<LexicalComposer>
<CodoxCollabPlugin />
<CodoxCommentPlugin /> // init comments plugin
</LexicalComposer>;

This setup allows the comment threads to become a part of the application state that Codox synchronizes across all active users. As users create new comment threads or comments, or delete existing ones, these actions are reflected in real-time across all sessions.

Initializing Comment Threads

Before you start a Codox session, it’s important to preload any existing comment threads associated with the document. This ensures that when users join the session, they have immediate access to all historical comments. To integrate this functionality, you can extend the existing startCodox function to include an initialization step for comment threads:

const [initLexicalState] = useState(null);
...
const startCodox = () => {
if (codoxAPI.current) {
codoxAPI.current
.initComments(initLexicalState.commentsThread);
codoxAPI.current
.start(codoxConfig)
.then(() => {
setCodoxStarted(true);
})
}
}

In this configuration, the commentsThread is assumed to be part of the initLexicalState. However, it’s not mandatory for it to be stored within this state. You can adapt this setup depending on where and how you store your application’s state related to comments. This approach not only ensures the continuity of the discussion threads but also enhances the collaborative experience by maintaining the context of conversations.

Persisting Thread

Codox is not strongly opinionated about how and where comment threads are persisted with the app’s architecture. We do however expect that any existing comment state be retrieved and made visible to codox via the initComments api call, and any changes to the comments will be saved.

When threads are created, delete, or updated (i.e. new comments added/old comments deleted), codox will notify the changes by invoking the contentChanged hook callback (if supplied) and emit the content_changed event. The developer will then decide what to do with the comments, for instance persisting the comment in the backend:

const codoxConfig = {
...
hooks: {
contentChanged: ({content, source}) => {
//contentChangedHookHandler(data);
},
},
};

The content parameter of the callback includes both the document’s root content and the commentThreads array:

const {
root, // content tree
commentThreads, // commentsThreads array - is empty when no comments or comments not used
} = content;

State Representation

Threaded comments are represented as an array where each element is a single thread identified by a codoxId. Comments within a thread are stored in an array, with each comment comprising content, author, timestamp, and codoxId:

const commentThreads = [
{
codoxId : "codox-d4877de8-88bf-43e2-9e9a-e178a74e997a"
comments : [{
author: "user_171",
content: "a comment",
codoxId: "codox-3555050f-3321-4e87-83a9-42f01aecb55e",
timeStamp: 282211.2999999523
},…
]
quote: "a quote"
type : "thread"
}
]

Comments UX

The plugin includes a default user interface that displays comment threads attached to the document. This UI is typically positioned to the right-hand side of the page. Through this interface, users can respond to or delete comments.

To initiate a new comment thread, the editor.dispatchCommand(INSERT_COMMENT) command is used, typically triggered when a user selects a portion of the text. The exact mechanism for triggering this through the UI depends on the specific design choices made by the application developer. When this command is issued, the CodoxCommentPlugin presents a text field or dialog adjacent to the highlighted text, where the user can enter the initial comment for the thread. We demonstrate how to implement this feature with a floating menu bar in our Extend Demo.

import {INSERT_COMMENT} from "@codoxhq/lexical-provider"
....
editor.dispatchCommand(INSERT_COMMENT) // will trigger modal open for comment add

Validating Node Nestings

In Lexical, there is no inherent enforcement of specific nesting combinations of native and third-party nodes. This flexibility can sometimes result in document corruption, where the document becomes unmanageable by plugins or core code, or the content is rendered incorrectly.

To prevent such issues, we have established a set of rules that restrict certain node nesting combinations. The table below details these prohibitions. For example, the first row specifies that a table node cannot be nested within another table, image, inline-image, or sticky node. The last line specifies that the Excalidraw plugin should not be inserted into the caption section of an image, inline-image, or sticky note.

Target (child lexical node types)Invalid Parents Nodes (to insert into)
“table”“table”, “image”, “inline-image”, “sticky”
“image”“image”, “inline-image”, “sticky”
“horizontalrule”“table”, “image”, “inline-image”, “sticky”
“page-break”“table”, “image”, “inline-image”, “sticky”
“collapsible-container”“table”, “image”, “inline-image”, “sticky”, “collapsible-container”
“layout-container”“table”, “image”, “inline-image”, “sticky”, “layout-container”
“inline-image”“image”, “inline-image”, “sticky”
“poll”“image”, “inline-image”, “sticky”
“excalidraw”“image”, “inline-image”, “sticky”

These rules are enforced in two ways:

  1. Existing content that contains these prohibited combinations will be rejected immediately when codox.start() is invoked.
  2. Any new content created or inserted by a user that violates these rules will be blocked, and the document will retain a valid state. If an onBlacklistedInsert callback is provided, it will be invoked to report the error.

The following utility function is included in the package to validate a document state before passing it to Codox:

let response = await fetch(/*url for fetching init state*/);
let initState = response.json();
// wrap in try-catch: validateStateStructure may throw if state is invalid
try {
// validate state from backend
validateStateStructure(initState, LEXICAL_NODES_TO_REGISTER); // if invalid - will throw
} catch(err) {
// capture error here
}
}, [])

onBlacklistedInsert

This optional callback is triggered each time a blacklisted content combination is detected and blocked. This allows the application to respond appropriately, for example, by notifying users that their actions are restricted:

const onBlackListedInsert = () => {
// Implement custom logic here, such as user notifications
};

Playground Plugin Polyfills

We have identified some behaviors in certain playground rich text plugins that were unforeseen in multiplayer contexts. To address these issues, we provide polyfills for these plugins when included in the Lexical setup:

Playground PluginPolyfill Plugin
FillBGColorPluginCodoxFillBGColorPlugin
FontColorPluginCodoxFontColorPlugin
import {CodoxFillBGColorPlugin} from "@codoxhq/lexical-provider"
import {CodoxFontColorPlugin} from "@codoxhq/lexical-provider"
// somewhere in toolbar plugin
<CodoxFillBGColorPlugin />
<CodoxFontColorPlugin />

Basic Codox Integration Sandbox

Demo playground

Extended Codox Integration Sandbox

Additional plugins included:

  • Comments
  • Undo

Demo playground

Github samples

GitHub