Skip to main content

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.

  1. In Node Constructors
class MyCustomNode extends ElementNode {
constructor(someData: string, key?: NodeKey) {
super(key); // Correctly passing key to parent constructor
this.__someData = someData;
}
}
  1. 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.

  1. Editor Methods
    // Get node by key
    const node = $getNodeByKey(key);

    // Get the DOM element a node key is reconciled to
    const element = editor.getElementByKey(key);

    // Get latest version of a node
    const latest = node.getLatest();

    // Get mutable version for updates
    const 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:

  1. Serialization

    • Keys are never serialized
    • New keys are generated when deserializing (from JSON/HTML)
    • Keys are only meaningful within their EditorState instance
  2. 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:

  1. Track nodes in the editor state
  2. Manage node updates and versions
  3. Maintain referential integrity
  4. Enable efficient state updates

Common Pitfalls​

  1. Key Reuse

    // ❌ Never do this
    function duplicateNode(node: LexicalNode) {
    return new SameNodeType(data, node.__key);
    }
  2. Manual Key Assignment

    // ❌ Never do this
    node.__key = generateCustomKey();
  3. Incorrect Constructor/Clone Implementation

    // ❌ Never do this - missing key in constructor
    class MyCustomNode extends ElementNode {
    constructor(someData: string) {
    super(); // Missing key parameter
    this.__someData = someData;
    }
    }

    // ✅ Correct implementation
    class 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;
    }
    }
  4. Node Replacement

    // ❌ Never re-use the key when changing the node class
    const editorConfig = {
    nodes: [
    CustomNodeType,
    {
    replace: OriginalNodeType,
    with: (node: OriginalNodeType) => new CustomNodeType(node.__key),
    withKlass: CustomNodeType
    }
    ]
    };

    // ✅ Correct: Use node replacement configuration
    const editorConfig = {
    nodes: [
    CustomNodeType,
    {
    replace: OriginalNodeType,
    with: (node: OriginalNodeType) => new CustomNodeType(),
    withKlass: CustomNodeType
    }
    ]
    };

    For proper node replacement, see the Node Replacement guide.

Best Practices​

  1. Let Lexical Handle Keys
    import {$applyNodeReplacement} from 'lexical';

    // Create node helper function
    export 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:

  1. Keys enable efficient node lookup (O(1))
  2. Proper key usage prevents unnecessary re-renders
  3. Lexical's key system optimizes state updates
  4. 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.