mrkeyoor.com_
Sat 08 Aug 20:59 UTC
npmWeb Frontendupdated 08 Aug 2026

@lexical/react

@lexical/react is the official React integration layer for Meta's Lexical editor framework. It supplies a composer context, content-editable components, error boundaries, hooks, and plugins for plain text, rich text, history, lists, links, tables, Markdown shortcuts, collaboration, menus, and other editing behaviors. Lexical keeps an immutable, serializable editor-state tree separate from the DOM; React components configure the editor and connect that state to application UI rather than making the editor itself controlled by React props.

Verdict

Lexical is an excellent foundation for a team intentionally building an editor, not a shortcut to a complete editing product. Choose it for its state model, command system, performance, and customization, then budget for schema design, UI, serialization tests, accessibility, migrations, and collaboration infrastructure.

API stability3/5The core composer, plugin, editor-state, update, command, and node concepts are consistent, but the package is still at 0.49.0 and actively adds replacement APIs such as extensions, NodeState, DOM import and render extensions, and BEFORE command priorities. Exact-version alignment across the many `@lexical/*` packages and careful release-note review remain important.
Docs5/5The official site explains React setup, editor state, updates, listeners, commands, priorities, custom nodes, JSON, HTML, Markdown, collaboration, browser support, accessibility, and headless operation. It includes focused examples and a full playground. The docs are candid about one-time initialization, discrete updates, Yjs ownership, custom serialization, and cleanup.
Maintenance5/5Version 0.49.0 was published on July 30, 2026, and GitHub reports a repository push on August 8, 2026. The monorepo is active, tested across unit and browser suites, and current documentation references newly added 0.44+ command priorities and extension APIs. Its 370 open issues and pull requests reflect a large fast-moving project rather than inactivity.
Ecosystem5/5@lexical/react recorded 4,291,246 downloads in the measured week, and the repository has 23,745 stars. Official packages cover rich text, lists, links, tables, Markdown, HTML, history, overflow, accessibility, Yjs, headless use, and devtools, while the playground and gallery provide substantial patterns for custom editors and collaboration.

Use it if

  • You are building a custom React editor and need explicit control over its node model, toolbar, commands, serialization, and plugins
  • You need structured JSON state plus HTML or Markdown import and export instead of treating contenteditable HTML as the source of truth
  • You need custom block or inline nodes, nested editors, tables, mentions, links, or decorator components
  • You plan collaborative editing with Yjs and are prepared to choose, secure, and operate a provider
Skip it if

Setup reality

The documented starting point is `npm install lexical @lexical/react`. Version 0.49.0 also declares peer requirements for React 18+, React DOM 18+, TypeScript 5.2+, and Yjs 13.5.22+; its export map intentionally sends older TypeScript versions to an error declaration. The React package installs 20 direct dependencies across Lexical feature packages plus Floating UI and devtools core, although applications import narrow subpaths such as `@lexical/react/LexicalComposer`. There are no native builds, credentials, or required config files for a local editor. You must provide `initialConfig` with a unique namespace and error handler, register every non-core node type up front, wrap plugins in `LexicalComposer`, render `ContentEditable`, supply an error boundary, and write all visual styling yourself. Rich text is not enabled merely by mounting the composer; add `RichTextPlugin` and the node classes and plugins for lists, links, tables, code, Markdown, or history that your schema needs. The source of truth is `EditorState`, not the DOM and not a React value prop. Dollar-prefixed functions must run inside `editor.update()`, `editor.read()`, or an editor-state `read()` closure. Updates normally reconcile asynchronously, so immediate server-side serialization after a mutation needs `{ discrete: true }`. `initialConfig.editorState` is consumed only once; changing the prop later does nothing, and collaboration requires `editorState: null` so Yjs owns initialization. OnChange persistence should be debounced and versioned because JSON includes node types and custom-node data that future code must still deserialize. Listeners and commands return unregister functions that React effects must return. HTML import needs DOMParser in the browser or a DOM implementation in headless Node, and full-fidelity custom CSS may need node replacements. CollaborationPlugin does not provide networking, authentication, authorization, awareness storage, document retention, or offline policy; those belong to the selected Yjs provider and your backend. For SSR, render the interactive editor in a client component and avoid making server markup disagree with the one-time initial editor state.

Patterns

Compose a plain-text editorcreate-plain-editor

import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { PlainTextPlugin } from '@lexical/react/LexicalPlainTextPlugin';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';

const config = {
  namespace: 'CommentEditor',
  onError(error) { throw error; },
};

export function Editor() {
  return (
    <LexicalComposer initialConfig={config}>
      <PlainTextPlugin
        contentEditable={<ContentEditable aria-label="Comment" />}
        placeholder={<div>Write a comment</div>}
        ErrorBoundary={LexicalErrorBoundary}
      />
      <HistoryPlugin />
    </LexicalComposer>
  );
}

Lexical supplies behavior, not appearance. Style the editable surface, placeholder, focus state, and error state in your application.

Register nodes for rich textconfigure-rich-text

import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { ListItemNode, ListNode } from '@lexical/list';
import { LinkNode } from '@lexical/link';

const config = {
  namespace: 'ArticleEditor',
  nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode],
  theme,
  onError(error) { console.error(error); },
};

<LexicalComposer initialConfig={config}>
  <RichTextPlugin
    contentEditable={<ContentEditable aria-label="Article body" />}
    ErrorBoundary={LexicalErrorBoundary}
  />
  <HistoryPlugin />
  <ListPlugin />
  <LinkPlugin />
</LexicalComposer>

A plugin can only create node types registered in `initialConfig.nodes`; keep all `@lexical/*` packages on the same version.

Get the editor inside a React pluginaccess-editor-instance

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

function FocusOnMountPlugin() {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    editor.focus();
  }, [editor]);
  return null;
}

The hook must run under a LexicalComposer. Put behavior in plugin components instead of trying to control the editor through composer props.

Observe and save editor statepersist-json-state

import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';

function SavePlugin({ save }) {
  return (
    <OnChangePlugin
      ignoreSelectionChange
      onChange={(editorState) => {
        save(JSON.stringify(editorState));
      }}
    />
  );
}

Debounce real persistence and version your stored document format. Custom node types must remain registered when that JSON is loaded later.

Initialize from stored JSONload-initial-state

const config = {
  namespace: 'ArticleEditor',
  editorState: storedJson ?? undefined,
  nodes: [HeadingNode, QuoteNode],
  onError(error) { throw error; },
};

<LexicalComposer initialConfig={config}>
  <EditorPlugins />
</LexicalComposer>

`editorState` is read only when the editor is created. `undefined` creates the default paragraph; `null` leaves an empty root for collaboration ownership.

Apply JSON after initializationreplace-editor-state

const [editor] = useLexicalComposerContext();

function loadDocument(json) {
  const nextState = editor.parseEditorState(json);
  if (!nextState.isEmpty()) {
    editor.setEditorState(nextState);
  }
}

Changing `initialConfig` will not reload content. `setEditorState` throws for a state whose root has no nodes and no selection.

Update the document modelinsert-content

import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical';

editor.update(() => {
  const paragraph = $createParagraphNode();
  paragraph.append($createTextNode('Hello from Lexical'));
  $getRoot().append(paragraph);
});

Dollar-prefixed helpers must run inside an update or read closure. Never mutate editor nodes from ordinary React render code.

Dispatch a toolbar formatting commandformat-selection

import { FORMAT_TEXT_COMMAND } from 'lexical';

function BoldButton() {
  const [editor] = useLexicalComposerContext();
  return (
    <button
      type="button"
      onMouseDown={(event) => event.preventDefault()}
      onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}
    >
      Bold
    </button>
  );
}

Preventing the toolbar mouse-down default keeps the editor selection from being lost before the click command runs.

Register and clean up a typed commandregister-custom-command

const INSERT_TOKEN_COMMAND = createCommand('INSERT_TOKEN_COMMAND');

function TokenPlugin() {
  const [editor] = useLexicalComposerContext();
  useEffect(() => {
    return editor.registerCommand(
      INSERT_TOKEN_COMMAND,
      (label) => {
        $insertNodes([$createTextNode(`{{${label}}}`)]);
        return true;
      },
      COMMAND_PRIORITY_EDITOR,
    );
  }, [editor]);
  return null;
}

Return the unregister function from the effect. A listener that returns `true` stops lower-priority command propagation.

Generate HTML from editor stateexport-html

import { $generateHtmlFromNodes } from '@lexical/html';

const html = editor.getEditorState().read(() => {
  return $generateHtmlFromNodes(editor, null);
});

Custom nodes control their own export behavior. JSON is the safer canonical storage format when HTML cannot represent application-specific node data.

Insert nodes parsed from HTMLimport-html

import { $generateNodesFromDOM } from '@lexical/html';
import { $getRoot, $insertNodes } from 'lexical';

const dom = new DOMParser().parseFromString(html, 'text/html');
editor.update(() => {
  const nodes = $generateNodesFromDOM(editor, dom);
  $getRoot().select();
  $insertNodes(nodes);
});

Sanitize hostile HTML before or during conversion and test style fidelity. Node import rules decide what survives.

Let a Yjs document own initial stateinitialize-collaboration

const config = {
  namespace: 'SharedDocument',
  editorState: null,
  nodes: collaborativeNodes,
  onError(error) { throw error; },
};

<LexicalComposer initialConfig={config}>
  <CollaborationPlugin
    id={documentId}
    providerFactory={providerFactory}
    shouldBootstrap={true}
  />
</LexicalComposer>

`editorState: null` is intentional for collaboration. You still must secure the provider, authorize document IDs, persist Yjs updates, and manage awareness.

Alternatives

PackageRegistryPick it when
@tiptap/reactnpmYou want a larger extension ecosystem and a higher-level ProseMirror-based React editor API
slate-reactnpmYou want React-first rendering over a deeply customizable JSON document model and accept more normalization work
draft-jsnpmYou maintain an existing Draft.js editor and migration cost outweighs the benefits of a newer model