Learn how to safely make changes to the Lexical editor state using update and read methods, and understand the $ function convention.
In Lexical, all state changes must happen within an update context. The editor.update() and editor.read() methods provide these contexts, along with access to special $ functions that can only be called within them.
type EditorUpdateOptions = { /** Callback to run after update completes */ onUpdate?: () => void; /** Skip transforms for this update */ skipTransforms?: true; /** Tag(s) to identify this update */ tag?: UpdateTag | UpdateTag[]; /** Force synchronous execution (no batching) */ discrete?: true;};
Multiple synchronous update() calls are automatically batched:
editor.update(() => { $getRoot().append($createParagraphNode());});editor.update(() => { $getRoot().append($createParagraphNode());});editor.update(() => { $getRoot().append($createParagraphNode());});// All three updates are batched into a single reconciliation
editor.update(() => { const root = $getRoot(); root.append($createParagraphNode()); // This reflects the pending state (with new paragraph) console.log(root.getChildrenSize()); // But transforms haven't run yet!});
To get the latest reconciled state within an update:
editor.update(() => { // Make changes... $getRoot().clear(); // Read reconciled state editor.getEditorState().read(() => { // This is the previous reconciled state const root = $getRoot(); });});
Calling editor.read() inside editor.update() flushes pending updates first. This can cause unexpected behavior.
console.log('Before update');editor.update(() => { console.log('During update'); $getRoot().clear();});console.log('After update');// Output:// Before update// During update// After update
// ❌ Bad - don't do thiseditor.update(async () => { const data = await fetchData(); $getRoot().clear();});// ✅ Good - fetch first, then updateasync function updateWithData() { const data = await fetchData(); editor.update(() => { $getRoot().clear(); // Use data... });}
// ✅ Better - signals intentconst text = editor.read(() => { return $getRoot().getTextContent();});// Works but less clearconst text = editor.update(() => { return $getRoot().getTextContent();});
Understand update timing
Updates are synchronous but batched:
editor.update(() => { /* change 1 */ });editor.update(() => { /* change 2 */ });// Both changes are applied in a single reconciliation// Use discrete to force immediate applicationeditor.update(() => { /* urgent change */ }, { discrete: true });