Key Management
Keys are a fundamental concept in Lexical that enable efficient state management and node tracking. Understanding how keys work is crucial for building reliable editor implementations.
What are Keys?
The __key property is a unique identifier assigned to each node in the Lexical editor. These keys are:
- Automatically generated by Lexical
- Used to track nodes in the editor state
- Essential for state management and updates
- Immutable during a node's lifecycle
When to Use __key?
✅ Correct Usage
Pass __key through constructors and clone methods in these two situations.
To reference a node from application code, use node.getKey() and the
key-related APIs instead of accessing __key directly.
- In Node Constructors
class MyCustomNode extends ElementNode {
constructor(someData: string, key?: NodeKey) {
super(key); // Correctly passing key to parent constructor
this.__someData = someData;
}
}
- In Static Clone Methods
class MyCustomNode extends ElementNode {
static clone(node: MyCustomNode): MyCustomNode {
return new MyCustomNode(node.__someData, node.__key);
}
}
❌ Incorrect Usage
Never use keys in these situations:
// ❌ Don't pass keys between different nodes
const newNode = new MyCustomNode(existingNode.__key);
// ❌ Don't manipulate keys directly
node.__key = 'custom-key';
How Lexical Uses Keys
Diagram
The dotted outlines show nodes that are re-used in a zero-copy fashion from one EditorState to the next
Node Map Structure
The EditorState maintains a Map<NodeKey, LexicalNode> that tracks all nodes. Nodes refer to each other using keys in their internal pointers:
// Internal node structure (not for direct usage)
{
__prev: null | NodeKey,
__next: null | NodeKey,
__parent: null | NodeKey,
// __first, __last and __size are only for ElementNode to track its children
__first: null | NodeKey,
__last: null | NodeKey,
__size: number
}
These internal pointers maintain the tree structure and should never be manipulated directly.
Key-Related APIs
- Editor Methods
// Get node by keyconst node = $getNodeByKey(key);// Get the DOM element a node key is reconciled toconst element = editor.getElementByKey(key);// Get latest version of a nodeconst latest = node.getLatest();// Get mutable version for updatesconst mutable = node.getWritable();
Referencing a node in a later callback
A callback (for example, a React event handler or a promise continuation) can
run after its target node has been removed. A non-null JavaScript reference
does not guarantee that the node still exists in the active editor state.
Methods that use getLatest() or getWritable() can then throw
Lexical node does not exist in active editor state.
One approach is to capture node.getKey() while reading or updating the node,
then resolve that key when the callback runs. Keep the lookup, attachment check,
and mutation inside the same editor.update() callback:
import type {LexicalEditor, NodeKey} from 'lexical';
import {$getNodeByKey} from 'lexical';
function createSelectNodeCallback(editor: LexicalEditor, nodeKey: NodeKey) {
return () => {
editor.update(() => {
const node = $getNodeByKey(nodeKey);
if (node !== null && node.isAttached()) {
node.selectEnd();
}
});
};
}
$getNodeByKey() returns null when the key is absent from the active state.
A node can also still be in that state's node map after being detached, before
garbage collection removes it. For an action that targets a node in the
document, isAttached() checks that it is still connected to the root.
If existing code keeps a node reference, guard its use inside the update:
import type {LexicalEditor, LexicalNode} from 'lexical';
function createSelectNodeCallback(editor: LexicalEditor, node: LexicalNode) {
return () => {
editor.update(() => {
if (node.isAttached()) {
node.selectEnd();
}
});
};
}
Both $getNodeByKey() and isAttached() require an active read or update
context. Checking them before editor.update(), or keeping a check's result
across an await, does not validate the state in which the mutation runs.
For a read-only callback, perform the lookup, check, and read together inside
editor.read(). Use the editor that owns the node; keys are not persistent
identifiers for use across editors or serialization.
Key Lifecycle
NodeKeys are ephemeral and have several important characteristics:
-
Serialization
- Keys are never serialized
- New keys are generated when deserializing (from JSON/HTML)
- Keys are only meaningful within their EditorState instance
-
Uniqueness
- Keys are unique within an EditorState
- Current implementation uses serial numbers for debugging
- Should be treated as random and opaque values
- Never logically reused
Keys are used internally by Lexical to:
- Track nodes in the editor state
- Manage node updates and versions
- Maintain referential integrity
- Enable efficient state updates
Common Pitfalls
-
Key Reuse
// ❌ Never do thisfunction duplicateNode(node: LexicalNode) {return new SameNodeType(data, node.__key);} -
Manual Key Assignment
// ❌ Never do thisnode.__key = generateCustomKey(); -
Incorrect Constructor/Clone Implementation
// ❌ Never do this - missing key in constructorclass MyCustomNode extends ElementNode {constructor(someData: string) {super(); // Missing key parameterthis.__someData = someData;}}// ✅ Correct implementationclass MyCustomNode extends ElementNode {__someData: string;constructor(someData: string, key?: NodeKey) {super(key);this.__someData = someData;}static clone(node: MyCustomNode): MyCustomNode {return new MyCustomNode(node.__someData, node.__key);}afterCloneFrom(prevNode: this): void {super.afterCloneFrom(prevNode);this.__someData = prevNode.__someData;}} -
Node Replacement
// ❌ Never re-use the key when changing the node classconst editorConfig = {nodes: [CustomNodeType,{replace: OriginalNodeType,with: (node: OriginalNodeType) => new CustomNodeType(node.__key),withKlass: CustomNodeType}]};// ✅ Correct: Use node replacement configurationconst editorConfig = {nodes: [CustomNodeType,{replace: OriginalNodeType,with: (node: OriginalNodeType) => new CustomNodeType(),withKlass: CustomNodeType}]};For proper node replacement, see the Node Replacement guide.
Best Practices
- Let Lexical Handle Keys
import {$applyNodeReplacement} from 'lexical';// Create node helper functionexport function $createMyCustomNode(data: string): MyCustomNode {return $applyNodeReplacement(new MyCustomNode(data));}
Testing Considerations
When writing tests involving node keys:
test('node creation', async () => {
await editor.update(() => {
// ✅ Correct: Create nodes normally
const node = new MyCustomNode("test");
// ✅ Correct: Keys are automatically handled
expect(node.__key).toBeDefined();
expect(node.__key).not.toBe('');
});
});
Performance Impact
Understanding key management is crucial for performance:
- Keys enable efficient node lookup (O(1))
- Proper key usage prevents unnecessary re-renders
- Lexical's key system optimizes state updates
- Improper key manipulation can cause performance issues
Common Questions
Q: How do I reference a node later?
A: Capture node.getKey() and resolve it inside the later read or update. If you keep a node reference, its methods resolve the latest version, but the node may have been removed. See Referencing a node in a later callback for both patterns and the checks needed before acting on a node in the document.
Q: How do I ensure unique nodes? A: Let Lexical handle key generation and management. Focus on node content and structure.