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