Skip to main content

@lexical/extension/HMRExtension

Interfaces

HMRConfig

Defined in: packages/lexical-extension/src/HMRExtension.ts:69

Configuration for HMRExtension.

Properties

hot

hot: HotContext | null

Defined in: packages/lexical-extension/src/HMRExtension.ts:74

The bundler's HMR context, typically import.meta.hot. Pass null in production or when HMR is not available.

id?

optional id?: string

Defined in: packages/lexical-extension/src/HMRExtension.ts:90

Stable identifier for this editor instance. Must be stable across HMR reloads — do not use useId(), Math.random(), or any per-mount identifier (these generate a new value on every mount and will fail to match the key from the previous HMR cycle, preventing state restoration). Only needed when multiple editors share both the same import.meta.hot context and the same namespace (set via defineExtension({ namespace: '...' }) or createEditor({ namespace: '...' })); editors with distinct namespaces are isolated automatically, editors with no configured namespace all share one key, and a nested editor shares its parent's namespace and so needs an id of its own. Must be a non-empty string when provided; passing '' triggers a dev warning and is treated as no id. Both are escaped into the key, so either may contain any character.


HMROutput

Defined in: packages/lexical-extension/src/HMRExtension.ts:54

The output of HMRExtension.

Properties

restoreCount

restoreCount: ReadonlySignal<number>

Defined in: packages/lexical-extension/src/HMRExtension.ts:65

Increments every time this editor's state has been restored from the module instance that was replaced.

Anything that derives editor state from somewhere else has to run again afterwards, and can depend on this signal to be told when: a nested editor wired up by SharedHistoryExtension re-points its HistoryState at its parent's, which a restore would otherwise have replaced with one of its own.


HotContext

Defined in: packages/lexical-extension/src/HMRExtension.ts:44

Minimal interface for bundler HMR contexts. Satisfied by Vite's ViteHotContext and similar bundler HMR contexts. Only the data property is read and written; other HMR lifecycle methods are not required.

Webpack and Parcel expose module.hot instead of import.meta.hot. Their module.hot.data is populated by dispose handlers and is not directly mutable, so module.hot cannot be passed here — a custom adapter using module.hot.addDisposeHandler is required for those bundlers.

Properties

data

readonly data: Record<string, unknown>

Defined in: packages/lexical-extension/src/HMRExtension.ts:45

Variables

HMRExtension

const HMRExtension: LexicalExtension<HMRConfig, "@lexical/extension/HMR", HMROutput, HMRInit>

Defined in: packages/lexical-extension/src/HMRExtension.ts:528

Preserves editor state, the selection, editability, and undo/redo history across Hot Module Replacement (HMR) cycles. When HistoryExtension is present as a peer, undo/redo stacks are preserved as well.

Passing hot: null is a safe no-op, so import.meta.hot ?? null works correctly in both development and production without a build-time conditional. If a saved state cannot be parsed, the extension warns in dev and falls back to $initialEditorState rather than throwing.

Editor updates only stash a reference to the current EditorState (and to the HistoryState, which @lexical/history mutates in place), so the per-update cost does not grow with the size of the document or of the undo stack. Everything is serialized once, by the module instance that replaces this one, when it restores the saved state.

The editor state and the history entries are serialized as one family rather than one at a time, so that the nodes they shared come back shared and every version of a node keeps answering to one key — see editorStateFamily. That is what makes an undo after a reload a diff of what changed rather than a rebuild of the document, and it is why the selection can be carried by key.

Examples

Basic usage

import {buildEditorFromExtensions, configExtension, defineExtension, HMRExtension} from '@lexical/extension';
import {RichTextExtension} from '@lexical/rich-text';
import {HistoryExtension} from '@lexical/history';

const editor = buildEditorFromExtensions(
defineExtension({
name: '[root]',
namespace: 'my-editor',
dependencies: [
RichTextExtension,
HistoryExtension,
configExtension(HMRExtension, {hot: import.meta.hot ?? null}),
],
}),
);

Multiple editors sharing an HMR context Editors with distinct namespace values are isolated automatically. Only add id when two editors share both the same import.meta.hot context and the same namespace.

// Different namespaces — automatic isolation, no `id` needed
defineExtension({ name: '[main]', namespace: 'main', dependencies: [configExtension(HMRExtension, {hot: import.meta.hot ?? null})] })
defineExtension({ name: '[sidebar]', namespace: 'sidebar', dependencies: [configExtension(HMRExtension, {hot: import.meta.hot ?? null})] })

// Same namespace — use `id` to distinguish
defineExtension({ name: '[first]', namespace: 'shared', dependencies: [configExtension(HMRExtension, {hot: import.meta.hot ?? null, id: 'first'})] })
defineExtension({ name: '[second]', namespace: 'shared', dependencies: [configExtension(HMRExtension, {hot: import.meta.hot ?? null, id: 'second'})] })

Only the undo/redo entries this editor recorded are preserved. With SharedHistoryExtension a nested editor pushes onto its parent's stacks, and an entry can only be applied to the editor that recorded it — editors the reload replaced, so those entries are left behind rather than re-pointed at whichever editor happens to restore the history.

A nested editor inherits its parent's namespace, so namespaces do not isolate it from its parent: give it a distinct id (or its own namespace), which is warned about in dev when both use HMRExtension.

An editor that was given no namespace at all is keyed without one, because the namespace createEditor generates for it is a fresh random string on every reload — a key built from that would never match what the previous instance saved. Such editors all share one key, so give each editor a namespace (or an id) as soon as a page has more than one.

Saved state belongs to the key for as long as the page lives, not to the reload that produced it. An editor that is unmounted and later remounted under the same key during development restores what the previous one had rather than its own $initialEditorState, so two editors showing different documents need distinct namespaces (or ids) even when they are never on screen at the same time.