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,
)