Skip to main content

Commands

Commands are a very powerful feature of Lexical that lets you register listeners for events like KEY_ENTER_COMMAND or KEY_TAB_COMMAND and contextually react to them wherever & however you'd like.

This pattern is useful for building Toolbars or complex Plugins and Nodes such as the TablePlugin which require special handling for selection, keyboard events, and more.

When registering a command you supply a priority and can return true to mark it as "handled", which stops other listeners from receiving the event. If a command isn't handled explicitly by you, it's likely handled by default in the RichTextPlugin or the PlainTextPlugin.

createCommand(...)

You can view all of the existing commands in LexicalCommands.ts, but if you need a custom command for your own use case check out the typed createCommand(...) function.

const HELLO_WORLD_COMMAND: LexicalCommand<string> = createCommand();

editor.dispatchCommand(HELLO_WORLD_COMMAND, 'Hello World!');

editor.registerCommand(
HELLO_WORLD_COMMAND,
(payload: string) => {
console.log(payload); // Hello World!
return false;
},
LowPriority,
);

editor.dispatchCommand(...)

Commands can be dispatched from anywhere you have access to the editor such as a Toolbar Button, an event listener, or a Plugin, but most of the core commands are dispatched from LexicalEvents.ts.

Calling dispatchCommand will implicitly call editor.update to trigger its command listeners if it was not called from inside editor.update.

editor.dispatchCommand(command, payload);

The payloads are typed via the createCommand(...) API, but they're usually a DOM event for commands dispatched from an event listener.

Here are some real examples from LexicalEvents.ts.

editor.dispatchCommand(KEY_ARROW_LEFT_COMMAND, event);
// ...
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');

And another example from the ToolbarPlugin in our Playground.

const formatBulletList = () => {
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND);
};

Which is later handled in useList to insert the list into the editor.

editor.registerCommand(
INSERT_UNORDERED_LIST_COMMAND,
() => {
insertList(editor, 'ul');
return true;
},
COMMAND_PRIORITY_LOW,
);

editor.registerCommand(...)

You can register a command from anywhere you have access to the editor object, but it's important that you remember to clean up the listener with its remove listener callback when it's no longer needed.

The command listener will always be called from an editor.update, so you may use dollar functions. You should not use editor.update (and never call editor.read) synchronously from within a command listener. It is safe to call editor.getEditorState().read if you need to read the previous state after updates have already been made.

const removeListener = editor.registerCommand(
COMMAND,
(payload) => boolean, // Return true to stop propagation.
priority,
);
// ...
removeListener(); // Cleans up the listener.

A common pattern for easy clean-up is returning a registerCommand call within a React useEffect.

useEffect(() => {
return editor.registerCommand(
TOGGLE_LINK_COMMAND,
(payload) => {
const url: string | null = payload;
setLink(url);
return true;
},
COMMAND_PRIORITY_EDITOR,
);
}, [editor]);

And as seen above and below, registerCommand's callback can return true to signal to the other listeners that the command has been handled and propagation will be stopped.

Here's a simplified example of handling a KEY_TAB_COMMAND from the RichTextPlugin, which is used to dispatch a OUTDENT_CONTENT_COMMAND or INDENT_CONTENT_COMMAND.

editor.registerCommand(
KEY_TAB_COMMAND,
(payload) => {
const event: KeyboardEvent = payload;
event.preventDefault();
return editor.dispatchCommand(
event.shiftKey ? OUTDENT_CONTENT_COMMAND : INDENT_CONTENT_COMMAND,
);
},
COMMAND_PRIORITY_EDITOR,
);

Note that the same KEY_TAB_COMMAND command is registered by LexicalTableSelectionHelpers.js, which handles moving focus to the next or previous cell within a Table, but the priority is the highest it can be (4) because this behavior is very important.