Skip to main content

Developing a Lexical Plugin in React

This guide explains how to create a standard Lexical plugin in React by structuring the code inside a /plugins/my-functionality folder.

📂 Folder Structure

The plugin is organized as follows:

/plugins/my-functionality
│── commands.ts
│── MyFunctionalityPlugin.tsx
│── MyFunctionalityNode.ts
│── MyFunctionalityComponent.tsx
│── utils.ts
│── interfaces.ts

📜 Implementation

1. commands.ts

This file defines custom commands that Lexical will use to interact with the plugin. A command should be named *SOME_ACTION*_COMMAND. When using the createCommand function, make sure to pass "*SOME_ACTION*_COMMAND" as argument. The argument is optional but it gives better command tracking, especially in Lexical DevTool. And there is no need to define and export an interface/type for the command payload, let TypeScrit infer types for you.

✅ So do this:

import { createCommand } from "lexical";

export const DO_SOMETHING_COMMAND = createCommand<{ somePayload: any; }>("DO_SOMETHING_COMMAND");

❌ Instead of this:

import { createCommand } from "lexical";

export type MyCommandPayload = { somePayload: any };

export const DO_SOMETHING_COMMAND = createCommand<MyCommandPayload>();

2. MyFunctionalityNode.ts

This file defines the custom Node that represents the new functionality within Lexical.

import { addClassNamesToElement } from "@lexical/utils";
import { ElementNode, LexicalNode } from "lexical";

import type { EditorConfig, NodeKey, SerializedElementNode, Spread } from "lexical";

type SerializedMyFunctionalityNode = Spread<{ someProperty?: SomePropertyType }, SerializedElementNode>;

export class MyFunctionalityNode extends ElementNode {
__someProperty?: SomePropertyType;

/*
* Constructor
*/
constructor(key?: NodeKey, property?: SomePropertyType) {
super(key);
this.__someProperty = property;
}

/*
* Static methods
*/
static clone(node: MyFunctionalityNode) {
return new MyFunctionalityNode(node.__key);
}

static importJSON(serializedNode: SerializedMyFunctionalityNode) {
return $createMyFunctionalityNode().updateFromJSON(serializedNode);
}

static getType() {
return "my-functionality-node";
}

/*
* Lexical core methods
*/
createDOM(config: EditorConfig) {
const element = document.createElement("div");
addClassNamesToElement(element, config.theme.myFunctionalityElement);
return element;
}

updateDOM(prevNode: this) {
const conditionToUpdateDOM = /* calculate stuff with current and previous node */;
return conditionToUpdateDOM;
}

// New method introduced with v0.23.0
// Make sure to implement this function
updateFromJSON(serializedNode: SerializedMyFunctionalityNode): this {
return super.updateFromJSON(serializedNode).setSomeProperty(serializedNode.someProperty);
}

/*
* Getters & Setters
* Do not type them, let TypeScript infer the types
*/
getSomeProperty() {
return this.getLatest().__someProperty;
}

setSomeProperty(property: SomePropertyType | undefined) {
const self = this.getWritable();
self.__someProperty = property;
return self;
}
}

export function $createMyFunctionalityNode(property?: SomePropertyType) {
return new MyFunctionalityNode(undefined, property);
}

export function $isMyFunctionalityNode(node: LexicalNode | null | undefined): node is MyFunctionalityNode {
return node instanceof MyFunctionalityNode;
}

Please note that property setters should follow this pattern and return the writtable class instance. This make the setters compliant with new updateFromJSON method:

setSomeProperty(property: SomePropertyType | undefined) {
const self = this.getWritable();
self.__someProperty = property;
return self;
}

3. MyFunctionalityPlugin.ts

This file defines the plugin that registers associated commands within Lexical.

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
import { COMMAND_PRIORITY_EDITOR } from 'lexical';
import { useEffect } from 'react';

import { MyFunctionalityNode } from './MyFunctionalityNode';
import { DO_SOMETHING_COMMAND } from './commands';

export function MyFunctionalityPlugin(): JSX.Element | null {
const [editor] = useLexicalComposerContext();

useEffect(() => {
if (!editor.hasNodes([MyFunctionalityNode])) {
throw new Error('MyFunctionalityPlugin: MyFunctionalityNode is not registered!');
}

return mergeRegister(
editor.registerCommand(
DO_SOMETHING_COMMAND,
(payload) => {
/* your code here */
},
COMMAND_PRIORITY_EDITOR,
),
);
}, [editor]);

return null;
}

Note: there is no need to type the editor.registerCommand function. Let typescript infer the types!

✅ So do this:

editor.registerCommand(
DO_SOMETHING_COMMAND,
(payload) => {
/* payload will be properly inferred here 👍 */
},
COMMAND_PRIORITY_EDITOR,
)

❌ Instead of this:

editor.registerCommand<{ someProperty: any }>(
DO_SOMETHING_COMMAND,
(payload) => {
/* might create types inconsistencies with what's defined in the command 👎 */
},
COMMAND_PRIORITY_EDITOR,
)