Skip to main content

Overview

Lexical provides custom React hooks that simplify common editor operations and state management. These hooks handle subscriptions, cleanup, and React lifecycle integration automatically.

Core Hooks

useLexicalComposerContext

Accesses the editor instance from anywhere within the LexicalComposer tree. Returns: [LexicalEditor, LexicalComposerContextType]
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { $getRoot } from 'lexical';

function CustomComponent() {
  const [editor] = useLexicalComposerContext();
  
  const handleClick = () => {
    editor.update(() => {
      const root = $getRoot();
      // Perform mutations
    });
  };
  
  return <button onClick={handleClick}>Custom Action</button>;
}
This hook must be used within a LexicalComposer component. It will throw an error if used outside the composer tree.

useLexicalEditable

Subscribes to the editor’s editable state. Returns: boolean - Current editable state
import { useLexicalEditable } from '@lexical/react/useLexicalEditable';

function EditableIndicator() {
  const isEditable = useLexicalEditable();
  
  return (
    <div className="status">
      {isEditable ? '✏️ Editing' : '👁️ Read-only'}
    </div>
  );
}
Prefer this hook over manually observing with editor.registerEditableListener(), especially when using React StrictMode or concurrent features.

useLexicalIsTextContentEmpty

Determines if the editor’s text content is empty. Parameters:
  • editor: LexicalEditor - The editor instance
  • trim?: boolean - Whether to trim whitespace before checking (default: false)
Returns: boolean - Whether the content is empty
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useLexicalIsTextContentEmpty } from '@lexical/react/useLexicalIsTextContentEmpty';

function SubmitButton() {
  const [editor] = useLexicalComposerContext();
  const isEmpty = useLexicalIsTextContentEmpty(editor, true);
  
  return (
    <button disabled={isEmpty}>
      Submit
    </button>
  );
}

useLexicalNodeSelection

Manages selection state for a specific node. Parameters:
  • key: NodeKey - The key of the node to track
Returns: [boolean, (selected: boolean) => void, () => void]
  • isSelected - Whether the node is currently selected
  • setSelected - Function to set selection state
  • clearSelected - Function to clear the selection
import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';

function CustomNode({ nodeKey }: { nodeKey: string }) {
  const [isSelected, setSelected, clearSelected] = useLexicalNodeSelection(nodeKey);
  const [editor] = useLexicalComposerContext();
  
  const handleClick = () => {
    if (!isSelected) {
      setSelected(true);
    } else {
      clearSelected();
    }
  };
  
  return (
    <div 
      onClick={handleClick}
      className={isSelected ? 'selected' : ''}
    >
      Custom Node Content
    </div>
  );
}

Subscription Hooks

useLexicalSubscription

Generic hook for subscribing to editor values with automatic cleanup. Type Signature:
type LexicalSubscription<T> = {
  initialValueFn: () => T;
  subscribe: (callback: (value: T) => void) => () => void;
};

function useLexicalSubscription<T>(
  subscription: (editor: LexicalEditor) => LexicalSubscription<T>
): T;
Example:
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useLexicalSubscription } from '@lexical/react/useLexicalSubscription';
import { LexicalEditor } from 'lexical';

function useIsEditable(): boolean {
  const [editor] = useLexicalComposerContext();
  
  return useLexicalSubscription((editor: LexicalEditor) => ({
    initialValueFn: () => editor.isEditable(),
    subscribe: (callback) => {
      return editor.registerEditableListener(callback);
    },
  }));
}

Practical Examples

Word Count Display

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useState, useEffect } from 'react';
import { $getRoot } from 'lexical';

function WordCount() {
  const [editor] = useLexicalComposerContext();
  const [count, setCount] = useState(0);

  useEffect(() => {
    return editor.registerUpdateListener(({ editorState }) => {
      editorState.read(() => {
        const root = $getRoot();
        const text = root.getTextContent();
        const words = text.split(/\s+/).filter(Boolean);
        setCount(words.length);
      });
    });
  }, [editor]);

  return <div className="word-count">{count} words</div>;
}

Character Counter with Limit

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useLexicalIsTextContentEmpty } from '@lexical/react/useLexicalIsTextContentEmpty';
import { useState, useEffect } from 'react';
import { $getRoot } from 'lexical';

function CharacterCounter({ maxLength }: { maxLength: number }) {
  const [editor] = useLexicalComposerContext();
  const [charCount, setCharCount] = useState(0);
  const isEmpty = useLexicalIsTextContentEmpty(editor);

  useEffect(() => {
    return editor.registerUpdateListener(({ editorState }) => {
      editorState.read(() => {
        const root = $getRoot();
        const text = root.getTextContent();
        setCharCount(text.length);
      });
    });
  }, [editor]);

  const remaining = maxLength - charCount;
  const isOverLimit = remaining < 0;

  return (
    <div className={isOverLimit ? 'text-red-500' : 'text-gray-500'}>
      {remaining} / {maxLength} characters
    </div>
  );
}

Editor State Synchronization

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

function SyncToLocalStorage({ storageKey }: { storageKey: string }) {
  const [editor] = useLexicalComposerContext();

  useEffect(() => {
    return editor.registerUpdateListener(({ editorState, tags }) => {
      // Skip updates triggered by history (undo/redo)
      if (tags.has('history-merge')) {
        return;
      }

      const json = JSON.stringify(editorState.toJSON());
      localStorage.setItem(storageKey, json);
    });
  }, [editor, storageKey]);

  return null;
}

Custom Toolbar State

import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { useState, useEffect, useCallback } from 'react';
import { 
  $getSelection, 
  $isRangeSelection,
  SELECTION_CHANGE_COMMAND,
  COMMAND_PRIORITY_LOW,
  FORMAT_TEXT_COMMAND
} from 'lexical';
import { mergeRegister } from '@lexical/utils';

function Toolbar() {
  const [editor] = useLexicalComposerContext();
  const [isBold, setIsBold] = useState(false);
  const [isItalic, setIsItalic] = useState(false);
  const [isUnderline, setIsUnderline] = useState(false);

  const updateToolbar = useCallback(() => {
    const selection = $getSelection();
    if ($isRangeSelection(selection)) {
      setIsBold(selection.hasFormat('bold'));
      setIsItalic(selection.hasFormat('italic'));
      setIsUnderline(selection.hasFormat('underline'));
    }
  }, []);

  useEffect(() => {
    return mergeRegister(
      editor.registerUpdateListener(({ editorState }) => {
        editorState.read(() => {
          updateToolbar();
        });
      }),
      editor.registerCommand(
        SELECTION_CHANGE_COMMAND,
        () => {
          updateToolbar();
          return false;
        },
        COMMAND_PRIORITY_LOW
      )
    );
  }, [editor, updateToolbar]);

  return (
    <div className="toolbar">
      <button
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold')}
        className={isBold ? 'active' : ''}
      >
        Bold
      </button>
      <button
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic')}
        className={isItalic ? 'active' : ''}
      >
        Italic
      </button>
      <button
        onClick={() => editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline')}
        className={isUnderline ? 'active' : ''}
      >
        Underline
      </button>
    </div>
  );
}

Read-Only Toggle

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

function EditableToggle() {
  const [editor] = useLexicalComposerContext();
  const isEditable = useLexicalEditable();

  const toggleEditable = () => {
    editor.setEditable(!isEditable);
  };

  return (
    <button onClick={toggleEditable}>
      {isEditable ? '🔒 Lock' : '🔓 Unlock'}
    </button>
  );
}

External Editor Reference

import { useRef, useEffect } from 'react';
import { LexicalEditor } from 'lexical';
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { EditorRefPlugin } from '@lexical/react/LexicalEditorRefPlugin';

function App() {
  const editorRef = useRef<LexicalEditor | null>(null);

  useEffect(() => {
    // Access editor from outside the composer tree
    const timer = setInterval(() => {
      if (editorRef.current) {
        editorRef.current.read(() => {
          const state = editorRef.current!.getEditorState();
          console.log('Editor state:', state.toJSON());
        });
      }
    }, 5000);

    return () => clearInterval(timer);
  }, []);

  const handleSave = () => {
    if (editorRef.current) {
      const json = JSON.stringify(editorRef.current.getEditorState().toJSON());
      // Save to backend
    }
  };

  return (
    <>
      <button onClick={handleSave}>Save</button>
      <LexicalComposer initialConfig={config}>
        <EditorRefPlugin editorRef={editorRef} />
        {/* other plugins */}
      </LexicalComposer>
    </>
  );
}

Hook Best Practices

Use Built-in Hooks

Prefer built-in hooks like useLexicalEditable() over manually managing subscriptions.

Cleanup Automatically

Hooks handle cleanup automatically. Always return cleanup functions from useEffect.

Avoid Stale Closures

Include all dependencies in useEffect/useCallback dependency arrays.

Batch Updates

Use editor.update() to batch multiple mutations into a single update.
All Lexical hooks are designed to work correctly with React StrictMode and concurrent rendering.
Never call $ functions (like $getRoot(), $getSelection()) outside of editor.update(), editor.read(), or their callbacks. These functions require an active editor context.

Type Signatures

// useLexicalComposerContext
function useLexicalComposerContext(): [
  LexicalEditor,
  LexicalComposerContextType
];

// useLexicalEditable
function useLexicalEditable(): boolean;

// useLexicalIsTextContentEmpty
function useLexicalIsTextContentEmpty(
  editor: LexicalEditor,
  trim?: boolean
): boolean;

// useLexicalNodeSelection
function useLexicalNodeSelection(
  key: NodeKey
): [
  boolean,
  (selected: boolean) => void,
  () => void
];

// useLexicalSubscription
type LexicalSubscription<T> = {
  initialValueFn: () => T;
  subscribe: (callback: (value: T) => void) => () => void;
};

function useLexicalSubscription<T>(
  subscription: (editor: LexicalEditor) => LexicalSubscription<T>
): T;

LexicalComposer

Learn about the composer component and configuration

Plugins

Explore available plugins and create custom ones

Editor API

Dive into the editor instance methods

State Management

Understand EditorState and how to work with it

Build docs developers (and LLMs) love