Lexical editor in React: keep the editor alive and save its document state

For a new React integration, Lexical's current getting-started documentation uses LexicalExtensionComposer with a stable root extension. Editing behavior comes from extensions, while React renders the editable surface and your controls. Save Lexical's serialized document state through your application rather than treating the editing DOM as the stored record. Current React setup

An instrument-checkout instruction makes the state boundary visible. An author changes “Return the cable separately” to “Return the cable in the side pocket.” That correction should survive a React rerender, a successful save, and reopening the same instruction.

Use one stable editor configuration

Keep all Lexical packages on compatible, matching versions. The current extension-based documentation differs from older examples centered on LexicalComposer, an initialConfig, and a list of React behavior plugins. Use the integration supported by the version your application installs; avoid combining both setup styles in a single example.

This minimal application fragment uses the current extension approach:

import { defineExtension } from 'lexical';
import { RichTextExtension } from '@lexical/rich-text';
import { HistoryExtension } from '@lexical/history';
import { ClipboardDOMImportExtension } from '@lexical/clipboard';
import { LexicalExtensionComposer }
  from '@lexical/react/LexicalExtensionComposer';
import { ContentEditable }
  from '@lexical/react/LexicalContentEditable';

const checkoutExtension = defineExtension({
  name: 'checkout-instructions',
  namespace: 'checkout-instructions',
  dependencies: [
    RichTextExtension,
    HistoryExtension,
    ClipboardDOMImportExtension
  ]
});

export default function CheckoutEditor() {
  return (
    <LexicalExtensionComposer
      extension={checkoutExtension}
      contentEditable={null}>
      <ContentEditable
        aria-label="Instrument checkout instructions"
        style={{ minHeight: 180, padding: 16 }}
      />
    </LexicalExtensionComposer>
  );
}

The root extension is declared outside the component. Changing its reference recreates the editor, so constructing a new configuration on every render can discard the editing session. The composer owns the editor's React lifetime; the example uses contentEditable={null} to place an explicitly labeled editing surface itself.

This is source for an existing React build with those packages installed. Add the application's visible focus treatment and content styling. For formatting controls, wire Lexical commands through components inside the composer; a button's appearance alone does not provide an editing command.

Read the state at the Save action

A child component can obtain the editor from its context. For a Save action after normal author input:

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

function CaptureButton({ onCapture }) {
  const [editor] = useLexicalComposerContext();
  return (
    <button type="button" onClick={() => {
      const json = editor.read('latest', () =>
        JSON.stringify(editor.getEditorState().toJSON())
      );
      onCapture(json);
    }}>
      Capture document JSON
    </button>
  );
}

Render this inside the composer and supply onCapture. It returns a JSON string; persistence, errors, and the saved status belong to your application's save route. This does not generate an HTML file.

'latest' reads the most recently reconciled state without committing pending updates. If application code updates the document and immediately serializes it in the same operation, use the documented update/read timing deliberately. Lexical's editor-state documentation explains force-commit reads and discrete updates for that case. Do not assume a synchronous update callback means its state has already committed.

Load a document deliberately

The root extension's $initialEditorState provides initial content once. For an existing editor intentionally loading another saved document, Lexical documents parsing and applying serialized state:

const nextState = editor.parseEditorState(savedJson);
editor.setEditorState(nextState);

Resolve unsaved edits before replacement and use the same required node types. Do not feed each change back through setEditorState() as if the editor were a controlled text input. Catch invalid or incompatible saved content in the real application and preserve the original record for recovery.

Serialized JSON contains document nodes, not the current selection or runtime node keys. Keep application record identifiers separate from those temporary keys. HTML export is another representation and needs its own rendering/import policy; the rich-text representation guide illustrates why visible words and document structure need separate checks.

For the checkout instruction, type the side-pocket correction, trigger an unrelated UI rerender, capture, and reopen the saved record. Confirm that the actual reader sees the cable instruction with its intended formatting. A successful JSON capture is the point where your application's persistence check begins.

Keep reading