본문으로 건너뛰기
cccskills
무료GitHub에 공개

web-editor-lexical

Extensible text editor framework by Meta. Use when building a rich text editor on Lexical — editor setup, custom nodes, commands, transforms, and serialization.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(6)

  • SKILL.md13.6 KB
  • examples/core.md13.3 KB
  • examples/custom-nodes.md13.8 KB
  • examples/serialization.md10.2 KB
  • metadata.yaml487 B
  • reference.md6.8 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Lexical Editor Patterns

Quick Guide: Lexical is an editor framework rather than an editor: the core gives you a node tree, a selection model, a reconciler, a command bus and an update lifecycle, and everything else is a plugin. EditorState is immutable, so every read and every mutation happens inside an editor.update() or editor.read() closure, and the $-prefixed functions are the ones that require that context. Extend the tree through ElementNode, TextNode or DecoratorNode, and react to content through transforms rather than listeners. Current: v0.42.x, pre-1.0 — APIs still move between minors.

Detailed Resources:

  • examples/core.md — editor setup, plugins with cleanup, toolbars, transforms, persistence
  • examples/custom-nodes.md — ElementNode, TextNode and DecoratorNode classes, the NodeState and $config APIs
  • examples/serialization.md — JSON and HTML round trips, exportDOM/importDOM, headless editors
  • reference.md — package map, command priorities, built-in commands, node hierarchy, plugin list, custom-node checklist

Which path applies

  • A React app — LexicalComposer owns the editor and every plugin is a child component reaching it through useLexicalComposerContext(). Start at examples/core.md.
  • A server or a build step, with no DOM — createHeadlessEditor from @lexical/headless runs the same node classes for search indexing, email rendering and content transforms. Register the same nodes as the client. See examples/serialization.md.
  • Adding a content type — the work is a node class plus its registration, and the branch that matters is which base node it extends. See the decision framework below and examples/custom-nodes.md.

<critical_requirements>

Before writing Lexical code

Call $-prefixed functions inside an editor.update() or editor.read() closure. $getRoot, $getSelection, $createTextNode and their siblings read the active editor state from a context that only exists inside those closures; outside one they throw at runtime, and nothing catches it at compile time.

Register every custom node in initialConfig.nodes. An unregistered node throws or silently drops content the moment the editor meets it, including on deserialization of previously saved documents.

Return the unsubscribe function from the useEffect that registers a command, transform or listener. Every register* method hands one back, and dropping it leaks a listener per render.

Open every transform with a precondition that the mutation makes false. A transform that mutates its target unconditionally marks the node dirty, which re-triggers the transform and freezes the editor.

</critical_requirements>


Auto-detection: 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

Applies to:

  • Rich text editing with custom formatting and embedded content
  • Custom content types — mentions, embeds, callouts, polls — as node classes
  • Structured document output rather than an HTML string
  • Server-side or build-time processing of editor content
  • Collaborative editing, where Lexical supplies the binding point

Handled elsewhere:

  • Visual design of the editor — the theme maps class names onto nodes, and what those classes contain is settled by whatever owns styling
  • Where the serialized document goes — the editor hands back JSON, and the transport and store are not its concern
  • Real-time sync between clients — Lexical exposes the state to bind, and the sync layer itself is a separate concern
  • Sanitizing HTML entering or leaving the editor — $generateNodesFromDOM trusts what it is given

<philosophy>

Lexical ships a core and no editor. The tree, selection, reconciler, command bus and update lifecycle are the product; toolbars, lists, links, embeds and formatting are all plugins, including the ones Meta writes.

EditorState is immutable. The editor holds a frozen snapshot. editor.update() clones it, applies the closure's changes, and reconciles the difference to the DOM — which is why a stale read outside a closure has no state to read and throws.

A plugin is a React component. It renders as a child of <LexicalComposer>, reaches the editor through useLexicalComposerContext(), and registers its commands, transforms and listeners in a useEffect that returns their unsubscribes. Many plugins render null.

Commands are the bus. Typed commands with priority-ordered listeners let one plugin intercept or augment another's behaviour without either knowing about the other.

Content is typed nodes. A new kind of content is a new node class, not a new attribute.

Lexical is pre-1.0, so a project that needs a frozen API surface should weigh that before adopting it. Weigh the node model too: a DecoratorNode is a real node in the tree, so it serializes and moves with the content around it. An editor whose requirement is a purely visual overlay — highlights or annotations that must never enter the saved document — is asking for something Lexical's decorators do not do.

</philosophy>

<decision_framework>

Which node type to extend

Does the content contain child nodes?
├─ YES → ElementNode (paragraphs, blockquotes, callouts)
└─ NO → Is it text carrying extra formatting or behaviour?
    ├─ YES → TextNode (coloured text, mentions)
    └─ NO → Is it an embedded component (image, video, widget)?
        └─ YES → DecoratorNode, whose decorate() returns the component

Plugin, transform or listener

Does the reaction modify nodes?
├─ YES → Transform — runs before reconciliation, so one DOM update covers it
└─ NO → Is it observing state?
    ├─ YES → registerUpdateListener, which runs after reconciliation
    └─ NO → A command, for user actions and toolbar clicks

Which command priority

Base editor behaviour?          → COMMAND_PRIORITY_EDITOR (0)
An ordinary plugin?             → COMMAND_PRIORITY_LOW (1) or _NORMAL (2)
Must override other plugins?    → COMMAND_PRIORITY_HIGH (3), as table navigation does
Nothing else may win?           → COMMAND_PRIORITY_CRITICAL (4)

Higher runs first, and returning true stops propagation to everything below. Full table in reference.md.

</decision_framework>


<patterns>

Core patterns

Pattern 1: React editor setup

LexicalComposer takes one initialConfig and wraps the plugins as children. Define the config outside the component so it is not rebuilt every render.

const initialConfig = {
  namespace: "MyEditor",
  theme, // class names per node type
  onError, // rethrow, or report — see red flags
  nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode], // every node the plugins need
};

<LexicalComposer initialConfig={initialConfig}>
  <RichTextPlugin contentEditable={<ContentEditable />} ErrorBoundary={LexicalErrorBoundary} />
  <HistoryPlugin />
</LexicalComposer>;

Full code: examples/core.md


Pattern 2: The update lifecycle

editor.update() mutates, editor.read() observes, and the $ prefix marks the functions that need one of them open.

editor.update(() => {
  const paragraph = $createParagraphNode();
  paragraph.append($createTextNode("Hello world"));
  $getRoot().append(paragraph);
});

editor.read(() => $getRoot().getTextContent());

Updates batch synchronously and reconcile asynchronously; pass { discrete: true } when the DOM has to be committed before the next statement reads it.

Full code: examples/serialization.md


Pattern 3: The command system

Commands carry a typed payload and are dispatched from anywhere. Listeners register at a priority and return true to consume the command.

export const INSERT_IMAGE_COMMAND: LexicalCommand<{
  src: string;
  alt: string;
}> = createCommand("INSERT_IMAGE_COMMAND");

editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
  src: "/image.png",
  alt: "Photo",
});

Full code: examples/core.md


Pattern 4: A plugin as a React component

The plugin reads the editor from context and owns its registrations for the life of the component.

export function ToolbarPlugin() {
  const [editor] = useLexicalComposerContext();
  return (
    <button type="button" onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold")}>
      Bold
    </button>
  );
}

Full code: examples/core.md


Pattern 5: Node transforms

A transform runs on every dirty node of its type before reconciliation, which makes it the cheapest place to react to content. The precondition is what stops it re-triggering itself.

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));
  }
});

Leaf nodes transform first, then elements, then the root, and the whole cascade produces a single DOM reconciliation.

Full code: examples/core.md


Pattern 6: Custom nodes

Every custom node needs static getType(), static clone(), createDOM(), updateDOM(), exportJSON() / static importJSON(), and an entry in initialConfig.nodes. Private properties take a double-underscore prefix so minifiers leave them alone, and every one of them has to be JSON-serializable.

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

  static getType(): string {
    return "image";
  }
  static clone(node: ImageNode): ImageNode {
    return new ImageNode(node.__src, node.__key);
  }
  updateDOM(): boolean {
    return false; // the existing element can be reused
  }
  decorate(): JSX.Element {
    return <img src={this.__src} alt="" />;
  }
}

Reach a node through its $createXxxNode() factory rather than new, so $applyNodeReplacement can run.

Full code: examples/custom-nodes.md


Pattern 7: Serialization

JSON is the persistence format — it round-trips the whole tree including custom node properties. HTML is for display and interop, and it is lossy.

const jsonString = JSON.stringify(editor.getEditorState().toJSON());

editor.setEditorState(editor.parseEditorState(jsonString).clone(null));

editor.read(() => $generateHtmlFromNodes(editor, null));

Full code: examples/serialization.md

</patterns>

<red_flags>

Red flags

Breaks at runtime:

  • A $-function called outside editor.update() / editor.read() — throws, with no compile-time warning — move the call inside the closure
  • A custom node missing from initialConfig.nodes — the editor throws or drops the content when it meets the node — register it in the same place the plugin is added
  • A transform with no precondition — the mutation marks the node dirty, re-triggering the transform until the editor freezes — guard on the condition the mutation removes
  • A register* call whose unsubscribe is dropped — one leaked listener per render and stale references after unmount — return it from useEffect
  • A node property holding a function, Map or Set — serialization breaks quietly — keep every property JSON-serializable
  • Two nodes sharing a getType() string — deserialization resolves the wrong class — namespace the type
  • new MyNode() instead of $createMyNode() — bypasses $applyNodeReplacement, so any registered replacement never runs
  • A single-underscore node property — minifiers mangle it — use __
  • Direct DOM mutation of editor content — bypasses the reconciler and desyncs state from DOM — go through nodes and commands
  • editor.update() called from inside an update listener — breaks undo/redo history and forces an extra render — use a transform
  • console.log in onError with no rethrow — swallows every editor error — rethrow or report it

Surprising behaviour:

  • Updates batch synchronously but reconcile asynchronously, so a DOM measurement taken straight after an update reads the old layout unless the update passed { discrete: true }
  • setEditorState steals focus unless the state is passed through editorState.clone(null)
  • DecoratorNode.decorate() renders its component outside the normal React tree, so state and context in it need care
  • onError that does not rethrow lets Lexical attempt its own recovery, which can mask a broken node class
  • TextNode modes change deletion: "token" makes the text an immutable chip, "segmented" deletes it word by word
  • CSS transition never fires on node removal, because the reconciler removes the element rather than hiding it
  • The NodeState API (v0.26+) is experimental and can change without an extended deprecation

</red_flags>

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Hugging Face Inference SDK patterns for TypeScript/Node.js — InferenceClient setup, chat completion, text generation, streaming, embeddings, image generation, audio transcription, translation, summarization, and Inference Endpoints

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

LiteLLM proxy server setup, TypeScript client patterns via OpenAI SDK, model routing, fallbacks, load balancing, spend tracking, virtual keys, and production deployment

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

Serverless GPU compute platform for AI model deployment — web endpoints, GPU functions, model serving, and TypeScript client patterns

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

Local LLM inference with the Ollama JavaScript client -- chat, streaming, tool calling, vision, embeddings, structured output, model management, and OpenAI-compatible endpoint

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

Replicate SDK patterns for TypeScript/Node.js -- client setup, predictions, streaming, webhooks, file handling, model versioning, deployments, and training

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

Together AI SDK patterns for TypeScript — client setup, chat completions, streaming, structured output, function calling, embeddings, image generation, fine-tuning, and OpenAI-compatible endpoints

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

agents-inc의 스킬 모두 보기

이 스킬의 문제 신고