web-editor-lexical

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Lexical Editor Patterns

Lexical 编辑器模式

Quick Guide: Lexical is a lightweight (22kb min+gzip) extensible text editor framework. Use
LexicalComposer
for React setup with plugins as child components. Extend via the node system (ElementNode, TextNode, DecoratorNode), command system (createCommand + priorities), and transforms. EditorState is immutable -- all mutations happen inside
editor.update()
. Use
$
-prefixed functions only inside update/read closures. Current: v0.42.x (pre-1.0)

<critical_requirements>
快速指南: Lexical是一个轻量级(压缩后仅22kb)的可扩展文本编辑器框架。在React环境中使用
LexicalComposer
进行搭建,将插件作为子组件使用。可通过节点系统(ElementNode、TextNode、DecoratorNode)、命令系统(createCommand + 优先级)和转换机制进行扩展。EditorState是不可变的——所有修改操作都必须在
editor.update()
内部执行。仅在update/read闭包内使用
$
前缀的函数。当前版本:v0.42.x(1.0正式版前)

<critical_requirements>

CRITICAL: Before Using This Skill

重要提示:使用本技能前须知

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type
, named constants)
(You MUST call
$
-prefixed functions (
$getRoot
,
$getSelection
,
$createTextNode
) ONLY inside
editor.update()
or
editor.read()
closures -- calling them outside throws runtime errors)
(You MUST register custom nodes in the
nodes
array of
initialConfig
-- unregistered nodes cause silent failures or runtime errors)
(You MUST return a cleanup function from
useEffect
when registering commands, transforms, or listeners -- Lexical register methods return unsubscribe functions)
(You MUST include preconditions in transforms to prevent infinite loops -- a transform that unconditionally modifies its target node re-triggers itself)
</critical_requirements>

Auto-detection: Lexical, lexical, @lexical/react, @lexical/rich-text, @lexical/list, @lexical/code, @lexical/link, @lexical/html, @lexical/headless, LexicalComposer, EditorState, LexicalNode, ElementNode, TextNode, DecoratorNode, createCommand, dispatchCommand, registerCommand, COMMAND_PRIORITY, $getRoot, $getSelection, $createParagraphNode, $createTextNode, RichTextPlugin, OnChangePlugin, HistoryPlugin, useLexicalComposerContext, editor.update, editor.read, registerNodeTransform, exportJSON, importJSON, exportDOM, importDOM, NodeState, createState
When to use:
  • Building rich text editors with custom formatting and embedded content
  • Creating editors with custom node types (mentions, embeds, code blocks)
  • Implementing collaborative editing with operational transforms
  • Building structured content editors (not just plain text)
When NOT to use:
  • Plain text inputs without formatting (use a standard
    <textarea>
    )
  • Simple markdown editing without live preview (use a textarea with markdown parsing)
  • Editors that need pure decorations without document mutation (Lexical decorator nodes mutate content)
Key patterns covered:
  • React setup with LexicalComposer, plugins, and initialConfig
  • Node system: ElementNode, TextNode, DecoratorNode, custom nodes
  • Command system: createCommand, priorities, dispatching, propagation
  • Transforms for automatic node mutations
  • EditorState immutability and the update lifecycle
  • JSON and HTML serialization
Detailed Resources:
  • examples/core.md - Editor setup, plugins, commands, transforms
  • examples/custom-nodes.md - Custom ElementNode, TextNode, DecoratorNode, NodeState API
  • examples/serialization.md - JSON/HTML serialization, import/export, headless usage
  • reference.md - Decision frameworks, command priority table, anti-patterns

<philosophy>
所有代码必须遵循CLAUDE.md中的项目约定(短横线命名法、具名导出、导入顺序、
import type
、具名常量)
(你必须仅在
editor.update()
或
editor.read()
闭包内调用
$
前缀的函数(如
$getRoot
、
$getSelection
、
$createTextNode
)——在外部调用会触发运行时错误)
(你必须在
initialConfig
的
nodes
数组中注册自定义节点——未注册的节点会导致静默失败或运行时错误)
(注册命令、转换或监听器时,必须从
useEffect
中返回清理函数——Lexical的注册方法会返回取消订阅函数)
(转换中必须包含前置条件以防止无限循环——无条件修改目标节点的转换会反复触发自身)
</critical_requirements>

自动检测关键词: Lexical、lexical、@lexical/react、@lexical/rich-text、@lexical/list、@lexical/code、@lexical/link、@lexical/html、@lexical/headless、LexicalComposer、EditorState、LexicalNode、ElementNode、TextNode、DecoratorNode、createCommand、dispatchCommand、registerCommand、COMMAND_PRIORITY、$getRoot、$getSelection、$createParagraphNode、$createTextNode、RichTextPlugin、OnChangePlugin、HistoryPlugin、useLexicalComposerContext、editor.update、editor.read、registerNodeTransform、exportJSON、importJSON、exportDOM、importDOM、NodeState、createState
适用场景:
  • 构建带有自定义格式和嵌入内容的富文本编辑器
  • 创建带有自定义节点类型(提及、嵌入内容、代码块)的编辑器
  • 实现基于操作转换的协同编辑功能
  • 构建结构化内容编辑器(而非纯文本编辑器)
不适用场景:
  • 无需格式的纯文本输入(使用标准
    <textarea>
    即可)
  • 无需实时预览的简单Markdown编辑(使用带Markdown解析的文本域)
  • 仅需纯装饰效果而不修改文档内容的编辑器(Lexical的装饰节点会修改内容)
涵盖的核心模式:
  • 使用LexicalComposer、插件和initialConfig进行React环境搭建
  • 节点系统:ElementNode、TextNode、DecoratorNode、自定义节点
  • 命令系统:createCommand、优先级、调度、传播
  • 用于自动节点修改的转换机制
  • EditorState不可变性与更新生命周期
  • JSON和HTML序列化
详细资源:
  • examples/core.md - 编辑器搭建、插件、命令、转换
  • examples/custom-nodes.md - 自定义ElementNode、TextNode、DecoratorNode、NodeState API
  • examples/serialization.md - JSON/HTML序列化、导入/导出、无头模式使用
  • reference.md - 决策框架、命令优先级表、反模式

<philosophy>

Philosophy

设计理念

Lexical is an editor framework, not a batteries-included editor. The core is intentionally minimal -- it provides the node tree, selection, reconciler, command system, and update lifecycle. Everything else (toolbars, formatting, lists, links, embeds) is a plugin.
Key architectural principles:
  • Immutable EditorState: The editor maintains a frozen state snapshot. Mutations happen inside
    editor.update()
    closures that clone the state, apply changes, then reconcile to DOM.
  • $
    -function convention:
    Functions prefixed with
    $
    (like
    $getRoot()
    ,
    $getSelection()
    ) must run inside
    editor.update()
    or
    editor.read()
    closures -- similar to React hooks requiring a component context.
  • Plugin = React component: In the React binding, a plugin is a React component rendered as a child of
    <LexicalComposer>
    . It accesses the editor via
    useLexicalComposerContext()
    and registers commands/transforms/listeners in
    useEffect
    .
  • Command-driven architecture: User interactions and plugin communication flow through typed commands with priority-based listeners, enabling plugins to intercept or augment behavior.
  • Node-driven content model: Content is a tree of typed nodes. Custom content types (mentions, embeds, polls) are custom node classes.
When to use Lexical:
  • Rich text editing with custom formatting and embedded content
  • Content editors requiring structured output (not just HTML strings)
  • Editors needing accessibility and screen reader support
  • Applications requiring server-side rendering or headless processing of editor content
When NOT to use Lexical:
  • Simple text inputs (a
    <textarea>
    is simpler and lighter)
  • Editors needing pure decorations that don't affect document content
  • Projects requiring a stable 1.0 API (Lexical is pre-1.0, APIs may change)
</philosophy>
<patterns>
Lexical是一个编辑器框架,而非开箱即用的编辑器。核心部分刻意保持极简——它提供节点树、选择器、协调器、命令系统和更新生命周期。其他所有功能(工具栏、格式、列表、链接、嵌入内容)均以插件形式存在。
核心架构原则:
  • 不可变EditorState: 编辑器维护一个冻结的状态快照。修改操作在
    editor.update()
    闭包内执行,该闭包会克隆状态、应用更改,然后协调更新DOM。
  • $
    函数约定:
    前缀为
    $
    的函数(如
    $getRoot()
    、
    $getSelection()
    )必须在
    editor.update()
    或
    editor.read()
    闭包内运行——类似React钩子需要组件上下文。
  • 插件=React组件: 在React绑定中,插件是作为
    <LexicalComposer>
    子组件渲染的React组件。它通过
    useLexicalComposerContext()
    访问编辑器,并在
    useEffect
    中注册命令/转换/监听器。
  • 命令驱动架构: 用户交互和插件通信通过带优先级监听器的类型化命令进行流转,使插件能够拦截或增强行为。
  • 节点驱动的内容模型: 内容是类型化节点的树状结构。自定义内容类型(提及、嵌入内容、投票)通过自定义节点类实现。
何时使用Lexical:
  • 需要自定义格式和嵌入内容的富文本编辑
  • 需要结构化输出的内容编辑器(而非仅HTML字符串)
  • 需要无障碍支持和屏幕阅读器适配的编辑器
  • 需要服务端渲染或无头处理编辑器内容的应用
何时不使用Lexical:
  • 简单文本输入(
    <textarea>
    更简单、更轻量)
  • 仅需纯装饰效果而不影响文档内容的编辑器
  • 需要稳定1.0 API的项目(Lexical处于1.0前版本,API可能会变化)
</philosophy>
<patterns>

Core Patterns

核心模式

Pattern 1: React Editor Setup

模式1:React编辑器搭建

The minimal setup uses
LexicalComposer
wrapping plugin components. Each plugin is a React component that accesses the editor via context.
typescript
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";

const EDITOR_NAMESPACE = "MyEditor";

const theme = {
  paragraph: "editor-paragraph",
  text: {
    bold: "editor-text-bold",
    italic: "editor-text-italic",
  },
};

function onError(error: Error) {
  console.error(error);
}

const initialConfig = {
  namespace: EDITOR_NAMESPACE,
  theme,
  onError,
  nodes: [], // Register custom nodes here
};

export function Editor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <RichTextPlugin
        contentEditable={<ContentEditable className="editor-input" />}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <HistoryPlugin />
    </LexicalComposer>
  );
}
Why good: Plugins compose as children, initialConfig centralizes node registration and theming, error boundary catches update errors gracefully
See examples/core.md for the full setup with OnChangePlugin, AutoFocusPlugin, and custom plugins.

最简搭建方式是使用
LexicalComposer
包裹插件组件。每个插件都是通过上下文访问编辑器的React组件。
typescript
import { LexicalComposer } from "@lexical/react/LexicalComposer";
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
import { ContentEditable } from "@lexical/react/LexicalContentEditable";
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";

const EDITOR_NAMESPACE = "MyEditor";

const theme = {
  paragraph: "editor-paragraph",
  text: {
    bold: "editor-text-bold",
    italic: "editor-text-italic",
  },
};

function onError(error: Error) {
  console.error(error);
}

const initialConfig = {
  namespace: EDITOR_NAMESPACE,
  theme,
  onError,
  nodes: [], // 在此处注册自定义节点
};

export function Editor() {
  return (
    <LexicalComposer initialConfig={initialConfig}>
      <RichTextPlugin
        contentEditable={<ContentEditable className="editor-input" />}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <HistoryPlugin />
    </LexicalComposer>
  );
}
优势: 插件以子组件形式组合,initialConfig集中管理节点注册和主题,错误边界可以优雅捕获更新错误
查看examples/core.md获取包含OnChangePlugin、AutoFocusPlugin和自定义插件的完整搭建示例。

Pattern 2: The
$
-Function Convention and Update Lifecycle

模式2:
$
函数约定与更新生命周期

All state reads and mutations use
$
-prefixed functions inside
editor.update()
(mutable) or
editor.read()
(read-only) closures. This ensures state consistency and prevents stale reads.
typescript
import { $getRoot, $createParagraphNode, $createTextNode } from "lexical";

// Writing: editor.update() clones state, applies changes, reconciles DOM
editor.update(() => {
  const root = $getRoot();
  const paragraph = $createParagraphNode();
  const text = $createTextNode("Hello world");
  paragraph.append(text);
  root.append(paragraph);
});

// Reading: editor.read() provides safe read-only access
editor.read(() => {
  const root = $getRoot();
  const textContent = root.getTextContent();
});
Why good: Immutable state model prevents race conditions,
$
prefix signals context requirement (like React hooks), update batching minimizes DOM reconciliation
Gotcha:
$
-functions called outside update/read closures throw runtime errors. There is no compile-time check.

所有状态读取和修改操作都需在
editor.update()
(可修改)或
editor.read()
(只读)闭包内使用
$
前缀的函数。这确保了状态一致性,防止读取到过期状态。
typescript
import { $getRoot, $createParagraphNode, $createTextNode } from "lexical";

// 写入操作:editor.update()克隆状态、应用更改、协调更新DOM
editor.update(() => {
  const root = $getRoot();
  const paragraph = $createParagraphNode();
  const text = $createTextNode("Hello world");
  paragraph.append(text);
  root.append(paragraph);
});

// 读取操作:editor.read()提供安全的只读访问
editor.read(() => {
  const root = $getRoot();
  const textContent = root.getTextContent();
});
优势: 不可变状态模型防止竞态条件,
$
前缀标识上下文要求(类似React钩子),批量更新最小化DOM协调操作
注意事项: 在update/read闭包外调用
$
函数会触发运行时错误,且无编译时检查。

Pattern 3: Command System

模式3:命令系统

Commands are the communication bus between plugins, toolbars, and the editor core. Create typed commands, dispatch them from UI, and register listeners with priorities.
typescript
import {
  createCommand,
  COMMAND_PRIORITY_EDITOR,
  COMMAND_PRIORITY_LOW,
  type LexicalCommand,
} from "lexical";

// Create a typed command
export const INSERT_IMAGE_COMMAND: LexicalCommand<{
  src: string;
  alt: string;
}> = createCommand("INSERT_IMAGE_COMMAND");

// Dispatch from toolbar or UI
editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
  src: "/image.png",
  alt: "Photo",
});
Priority levels (higher number = runs first, can intercept):
PriorityValueUse case
COMMAND_PRIORITY_CRITICAL
4Emergency overrides
COMMAND_PRIORITY_HIGH
3Table navigation, critical plugins
COMMAND_PRIORITY_NORMAL
2Standard plugin behavior
COMMAND_PRIORITY_LOW
1Default for most plugins
COMMAND_PRIORITY_EDITOR
0Base editor behavior
Return
true
from a listener to stop propagation to lower-priority listeners.
See examples/core.md for the full command registration pattern with cleanup.

命令是插件、工具栏和编辑器核心之间的通信总线。创建类型化命令,从UI调度命令,并注册带优先级的监听器。
typescript
import {
  createCommand,
  COMMAND_PRIORITY_EDITOR,
  COMMAND_PRIORITY_LOW,
  type LexicalCommand,
} from "lexical";

// 创建类型化命令
export const INSERT_IMAGE_COMMAND: LexicalCommand<{
  src: string;
  alt: string;
}> = createCommand("INSERT_IMAGE_COMMAND");

// 从工具栏或UI调度命令
editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
  src: "/image.png",
  alt: "Photo",
});
优先级等级(数值越高越先执行,可拦截后续命令):
优先级数值适用场景
COMMAND_PRIORITY_CRITICAL
4紧急覆盖操作
COMMAND_PRIORITY_HIGH
3表格导航、关键插件
COMMAND_PRIORITY_NORMAL
2标准插件行为
COMMAND_PRIORITY_LOW
1大多数插件的默认优先级
COMMAND_PRIORITY_EDITOR
0编辑器基础行为
**返回
true
**可终止命令向低优先级监听器传播。
查看examples/core.md获取带清理函数的完整命令注册模式。

Pattern 4: Custom Plugins (React)

模式4:自定义插件(React)

A plugin is a React component that registers commands, transforms, or listeners via
useLexicalComposerContext
. Always return cleanup functions from
useEffect
.
typescript
import { useEffect } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import {
  COMMAND_PRIORITY_LOW,
  FORMAT_TEXT_COMMAND,
  type TextFormatType,
} from "lexical";

export function ToolbarPlugin() {
  const [editor] = useLexicalComposerContext();

  const handleFormat = (format: TextFormatType) => {
    editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
  };

  return (
    <div className="toolbar">
      <button onClick={() => handleFormat("bold")} type="button">
        Bold
      </button>
      <button onClick={() => handleFormat("italic")} type="button">
        Italic
      </button>
    </div>
  );
}
Why good: Plugin accesses editor through context hook, dispatches built-in FORMAT_TEXT_COMMAND, renders null or UI as needed
See examples/core.md for plugins that register commands with useEffect cleanup.

插件是通过
useLexicalComposerContext
注册命令、转换或监听器的React组件。务必从
useEffect
中返回清理函数。
typescript
import { useEffect } from "react";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import {
  COMMAND_PRIORITY_LOW,
  FORMAT_TEXT_COMMAND,
  type TextFormatType,
} from "lexical";

export function ToolbarPlugin() {
  const [editor] = useLexicalComposerContext();

  const handleFormat = (format: TextFormatType) => {
    editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
  };

  return (
    <div className="toolbar">
      <button onClick={() => handleFormat("bold")} type="button">
        粗体
      </button>
      <button onClick={() => handleFormat("italic")} type="button">
        斜体
      </button>
    </div>
  );
}
优势: 插件通过上下文钩子访问编辑器,调度内置的FORMAT_TEXT_COMMAND,可根据需要渲染空内容或UI
查看examples/core.md获取使用useEffect清理函数注册命令的插件示例。

Pattern 5: Node Transforms

模式5:节点转换

Transforms automatically mutate nodes when conditions are met. They run before DOM reconciliation, making them the most efficient way to react to content changes.
typescript
import { TextNode } from "lexical";

// Transform: auto-capitalize first letter of paragraphs
editor.registerNodeTransform(TextNode, (textNode) => {
  const text = textNode.getTextContent();
  // CRITICAL: Precondition prevents infinite loop
  if (text.length > 0 && text[0] !== text[0].toUpperCase()) {
    textNode.setTextContent(text[0].toUpperCase() + text.slice(1));
  }
});
Why preconditions matter: Without the check,
setTextContent
marks the node dirty, re-triggering the transform infinitely.
Transform execution order: Leaf nodes first, then element nodes, then RootNode. Multiple transforms produce a single DOM reconciliation.
See examples/core.md for transform registration with cleanup and use cases.

转换会在满足条件时自动修改节点。它们在DOM协调前运行,是响应内容变化最高效的方式。
typescript
import { TextNode } from "lexical";

// 转换:自动将段落首字母大写
editor.registerNodeTransform(TextNode, (textNode) => {
  const text = textNode.getTextContent();
  // 重要:前置条件防止无限循环
  if (text.length > 0 && text[0] !== text[0].toUpperCase()) {
    textNode.setTextContent(text[0].toUpperCase() + text.slice(1));
  }
});
前置条件的重要性: 若无此检查,
setTextContent
会标记节点为脏状态,反复触发转换导致无限循环。
转换执行顺序: 先叶子节点,再元素节点,最后RootNode。多个转换只会触发一次DOM协调。
查看examples/core.md获取带清理函数的转换注册及用例。

Pattern 6: Custom Nodes

模式6:自定义节点

Lexical provides three extendable base nodes for custom content types:
Base NodePurposeKey method
ElementNode
Container nodes (blockquote, callout)
createDOM()
,
updateDOM()
TextNode
Styled text variants (colored text, mentions)
createDOM()
,
updateDOM()
DecoratorNode<T>
Embedded components (images, videos, polls)
decorate()
returns a component
Every custom node requires:
  1. static getType()
    -- unique string identifier
  2. static clone(node)
    -- create copy for state snapshots
  3. createDOM()
    -- return the HTMLElement representation
  4. updateDOM()
    -- return
    false
    if existing DOM can be reused
  5. exportJSON()
    /
    static importJSON()
    -- serialization
  6. Registration in
    initialConfig.nodes
typescript
import { DecoratorNode } from "lexical";
import type { LexicalNode, NodeKey, EditorConfig } from "lexical";

export class ImageNode extends DecoratorNode<JSX.Element> {
  __src: string;
  __alt: string;

  static getType(): string {
    return "image";
  }

  static clone(node: ImageNode): ImageNode {
    return new ImageNode(node.__src, node.__alt, node.__key);
  }

  constructor(src: string, alt: string, key?: NodeKey) {
    super(key);
    this.__src = src;
    this.__alt = alt;
  }

  createDOM(_config: EditorConfig): HTMLElement {
    return document.createElement("div");
  }

  updateDOM(): boolean {
    return false;
  }

  decorate(): JSX.Element {
    return <img src={this.__src} alt={this.__alt} />;
  }
}
Property convention: Prefix private properties with
__
(double underscore) to prevent minification issues. All properties must be JSON-serializable.
See examples/custom-nodes.md for complete ElementNode, TextNode, DecoratorNode examples with serialization and the NodeState API.

Lexical提供三种可扩展的基础节点用于自定义内容类型:
基础节点用途核心方法
ElementNode
容器节点(块引用、提示框)
createDOM()
、
updateDOM()
TextNode
带样式的文本变体(彩色文本、提及)
createDOM()
、
updateDOM()
DecoratorNode<T>
嵌入组件(图片、视频、投票)
decorate()
返回React组件
每个自定义节点需要:
  1. static getType()
    -- 唯一字符串标识符
  2. static clone(node)
    -- 为状态快照创建副本
  3. createDOM()
    -- 返回HTMLElement表示
  4. updateDOM()
    -- 若现有DOM可复用则返回
    false
  5. exportJSON()
    /
    static importJSON()
    -- 序列化
  6. 在
    initialConfig.nodes
    中注册
typescript
import { DecoratorNode } from "lexical";
import type { LexicalNode, NodeKey, EditorConfig } from "lexical";

export class ImageNode extends DecoratorNode<JSX.Element> {
  __src: string;
  __alt: string;

  static getType(): string {
    return "image";
  }

  static clone(node: ImageNode): ImageNode {
    return new ImageNode(node.__src, node.__alt, node.__key);
  }

  constructor(src: string, alt: string, key?: NodeKey) {
    super(key);
    this.__src = src;
    this.__alt = alt;
  }

  createDOM(_config: EditorConfig): HTMLElement {
    return document.createElement("div");
  }

  updateDOM(): boolean {
    return false;
  }

  decorate(): JSX.Element {
    return <img src={this.__src} alt={this.__alt} />;
  }
}
属性约定: 私有属性前缀为
__
(双下划线)以防止压缩问题。所有属性必须可JSON序列化。
查看examples/custom-nodes.md获取完整的ElementNode、TextNode、DecoratorNode示例,包含序列化和NodeState API。

Pattern 7: EditorState Serialization

模式7:EditorState序列化

Lexical supports JSON (preferred for persistence) and HTML (for display or interop).
typescript
import { $generateHtmlFromNodes } from "@lexical/html";

// JSON: lossless round-trip
const json = editor.getEditorState().toJSON();
const jsonString = JSON.stringify(json);

// Restore from JSON
const editorState = editor.parseEditorState(jsonString);
editor.setEditorState(editorState);

// HTML: for rendering or export
editor.read(() => {
  const html = $generateHtmlFromNodes(editor, null);
});
JSON vs HTML: JSON preserves the full node tree and is the recommended format for persistence. HTML is lossy (loses custom node properties) but useful for display or email content.
See examples/serialization.md for complete import/export patterns, HTML-to-Lexical conversion, and headless editor usage.
</patterns>
<decision_framework>
Lexical支持JSON(推荐用于持久化)和HTML(用于展示或互操作)两种格式。
typescript
import { $generateHtmlFromNodes } from "@lexical/html";

// JSON:无损往返序列化
const json = editor.getEditorState().toJSON();
const jsonString = JSON.stringify(json);

// 从JSON恢复
const editorState = editor.parseEditorState(jsonString);
editor.setEditorState(editorState);

// HTML:用于渲染或导出
editor.read(() => {
  const html = $generateHtmlFromNodes(editor, null);
});
JSON vs HTML: JSON保留完整节点树,是推荐的持久化格式。HTML是有损的(丢失自定义节点属性),但适用于展示或邮件内容。
查看examples/serialization.md获取完整的导入/导出模式、HTML转Lexical转换和无头编辑器使用方法。
</patterns>
<decision_framework>

Decision Framework

决策框架

Which Node Type to Extend

选择要扩展的节点类型

Does your content contain child nodes?
├─ YES → ElementNode (paragraphs, blockquotes, callouts)
└─ NO → Is it text with special formatting or behavior?
    ├─ YES → TextNode (colored text, mentions)
    └─ NO → Is it an embedded component (image, video, widget)?
        ├─ YES → DecoratorNode (renders arbitrary UI)
        └─ NO → Re-evaluate: most content fits one of the above
你的内容包含子节点吗?
├─ 是 → ElementNode(段落、块引用、提示框)
└─ 否 → 是否是带有特殊格式或行为的文本?
    ├─ 是 → TextNode(彩色文本、提及)
    └─ 否 → 是否是嵌入组件(图片、视频、小部件)?
        ├─ 是 → DecoratorNode(渲染任意UI)
        └─ 否 → 重新评估:大多数内容都符合上述类型之一

Plugin vs Transform vs Listener

插件 vs 转换 vs 监听器

Need to react to content changes?
├─ YES → Does the reaction modify nodes?
│   ├─ YES → Transform (most efficient, runs before DOM reconciliation)
│   └─ NO → Update listener (read-only, runs after reconciliation)
└─ NO → Need to handle user actions or toolbar clicks?
    ├─ YES → Command (typed, priority-based, interceptable)
    └─ NO → Listener (registerUpdateListener for state observation)
需要响应内容变化吗?
├─ 是 → 响应操作是否会修改节点?
│   ├─ 是 → 转换(最高效,在DOM协调前运行)
│   └─ 否 → 更新监听器(只读,在协调后运行)
└─ 否 → 需要处理用户操作或工具栏点击吗?
    ├─ 是 → 命令(类型化、基于优先级、可拦截)
    └─ 否 → 监听器(注册updateListener观察状态)

Command Priority Selection

命令优先级选择

Is this the base editor behavior?
├─ YES → COMMAND_PRIORITY_EDITOR (0)
└─ NO → Is this a standard plugin?
    ├─ YES → COMMAND_PRIORITY_LOW (1) or COMMAND_PRIORITY_NORMAL (2)
    └─ NO → Must it override other plugins (e.g., table navigation)?
        ├─ YES → COMMAND_PRIORITY_HIGH (3)
        └─ NO → Emergency override only?
            └─ YES → COMMAND_PRIORITY_CRITICAL (4)
</decision_framework>

<red_flags>
这是编辑器基础行为吗?
├─ 是 → COMMAND_PRIORITY_EDITOR (0)
└─ 否 → 这是标准插件吗?
    ├─ 是 → COMMAND_PRIORITY_LOW (1) 或 COMMAND_PRIORITY_NORMAL (2)
    └─ 否 → 必须覆盖其他插件吗(如表格导航)?
        ├─ 是 → COMMAND_PRIORITY_HIGH (3)
        └─ 否 → 仅用于紧急覆盖?
            └─ 是 → COMMAND_PRIORITY_CRITICAL (4)
</decision_framework>

<red_flags>

RED FLAGS

注意事项

High Priority Issues:
  • Calling
    $
    -prefixed functions outside
    editor.update()
    or
    editor.read()
    -- causes runtime errors with no compile-time warning
  • Missing node registration in
    initialConfig.nodes
    -- custom nodes silently fail or throw when the editor encounters them
  • Transforms without preconditions -- unconditional mutations retrigger the transform infinitely, freezing the editor
  • Using
    editor.update()
    inside an update listener to modify state -- breaks undo/redo history and causes extra renders; use transforms instead
  • Forgetting
    useEffect
    cleanup for register calls -- leaks listeners, causes stale references after component unmount
Medium Priority Issues:
  • Direct DOM manipulation instead of using the node/command system -- bypasses the reconciler, causes state-DOM desync
  • Storing non-JSON-serializable values in node properties (functions, Maps, Sets) -- breaks serialization silently
  • Using
    editor.setEditorState()
    without cloning -- can cause unexpected focus changes; use
    editorState.clone(null)
    to prevent auto-focus
  • Naming custom node
    getType()
    with a non-unique string -- collides with other nodes, causes deserialization failures
  • Using
    new MyNode()
    directly instead of
    $createMyNode()
    factory -- bypasses the node replacement system (
    $applyNodeReplacement
    )
  • Single underscore node properties (
    _value
    instead of
    __value
    ) -- may be mangled by minifiers, breaking node access
  • console.log
    in
    onError
    callback with no rethrow -- silently swallows editor errors; rethrow or send to error tracking
Gotchas & Edge Cases:
  • editor.update()
    batches synchronously but reconciles asynchronously -- use
    { discrete: true }
    option when you need synchronous DOM commit (e.g., before reading DOM measurements)
  • Node property names must use
    __
    prefix convention -- single underscore properties may be mangled by minifiers
  • DecoratorNode.decorate()
    returns a component that Lexical renders outside the normal React tree -- state management in decorator components needs care
  • The
    onError
    callback in
    initialConfig
    receives errors from update closures -- if you don't rethrow, Lexical tries to recover gracefully
  • TextNode
    modes:
    "token"
    makes text immutable (like a chip),
    "segmented"
    deletes word-by-word
  • CSS
    transition
    does not work for animations based on node removal -- Lexical reconciles by removing DOM nodes, not hiding them
  • The NodeState API (v0.26+) is experimental -- APIs may change without extended deprecation
</red_flags>

<critical_reminders>
高优先级问题:
  • 在
    editor.update()
    或
    editor.read()
    外部调用
    $
    前缀函数——会触发无编译时警告的运行时错误
  • 未在
    initialConfig.nodes
    中注册节点——自定义节点会静默失败或在编辑器遇到时抛出错误
  • 转换无前置条件——无条件修改会反复触发转换,导致编辑器冻结
  • 在更新监听器内使用
    editor.update()
    修改状态——破坏撤销/重做历史,导致额外渲染;应使用转换代替
  • 注册调用时忘记
    useEffect
    清理——泄漏监听器,组件卸载后导致过期引用
中优先级问题:
  • 直接操作DOM而非使用节点/命令系统——绕过协调器,导致状态与DOM不一致
  • 在节点属性中存储不可JSON序列化的值(函数、Map、Set)——静默破坏序列化
  • 不克隆就使用
    editor.setEditorState()
    ——可能导致意外的焦点变化;使用
    editorState.clone(null)
    防止自动聚焦
  • 自定义节点
    getType()
    使用非唯一字符串——与其他节点冲突,导致反序列化失败
  • 直接使用
    new MyNode()
    而非
    $createMyNode()
    工厂函数——绕过节点替换系统(
    $applyNodeReplacement
    )
  • 节点属性使用单下划线(
    _value
    而非
    __value
    )——可能被压缩器混淆,破坏节点访问
  • onError
    回调中仅用
    console.log
    而不重新抛出——静默吞没编辑器错误;应重新抛出或发送到错误跟踪系统
注意事项与边缘情况:
  • editor.update()
    同步批量处理但异步协调DOM——当需要同步提交DOM时(如读取DOM尺寸前)使用
    { discrete: true }
    选项
  • 节点属性名称必须使用
    __
    前缀约定——单下划线属性可能被压缩器混淆
  • DecoratorNode.decorate()
    返回的组件由Lexical在正常React树外部渲染——装饰组件的状态管理需要注意
  • initialConfig
    中的
    onError
    回调接收update闭包中的错误——若不重新抛出,Lexical会尝试优雅恢复
  • TextNode模式:
    "token"
    使文本不可变(类似芯片),
    "segmented"
    支持逐词删除
  • 基于节点移除的动画无法使用CSS
    transition
    ——Lexical通过移除DOM节点进行协调,而非隐藏
  • NodeState API(v0.26+)处于实验阶段——API可能会在无长期弃用通知的情况下更改
</red_flags>

<critical_reminders>

CRITICAL REMINDERS

重要提醒

All code must follow project conventions in CLAUDE.md
(You MUST call
$
-prefixed functions (
$getRoot
,
$getSelection
,
$createTextNode
) ONLY inside
editor.update()
or
editor.read()
closures -- calling them outside throws runtime errors)
(You MUST register custom nodes in the
nodes
array of
initialConfig
-- unregistered nodes cause silent failures or runtime errors)
(You MUST return a cleanup function from
useEffect
when registering commands, transforms, or listeners -- Lexical register methods return unsubscribe functions)
(You MUST include preconditions in transforms to prevent infinite loops -- a transform that unconditionally modifies its target node re-triggers itself)
Failure to follow these rules will cause runtime errors, memory leaks, frozen editors, and broken undo/redo history.
</critical_reminders>
所有代码必须遵循CLAUDE.md中的项目约定
(你必须仅在
editor.update()
或
editor.read()
闭包内调用
$
前缀的函数(如
$getRoot
、
$getSelection
、
$createTextNode
)——在外部调用会触发运行时错误)
(你必须在
initialConfig
的
nodes
数组中注册自定义节点——未注册的节点会导致静默失败或运行时错误)
(注册命令、转换或监听器时,必须从
useEffect
中返回清理函数——Lexical的注册方法会返回取消订阅函数)
(转换中必须包含前置条件以防止无限循环——无条件修改目标节点的转换会反复触发自身)
不遵循这些规则会导致运行时错误、内存泄漏、编辑器冻结和撤销/重做历史损坏。
</critical_reminders>