[lexical-extension][lexical-react] Feature: HMR support for extensions - #8959
Open
mayrang wants to merge 3 commits into
Open
[lexical-extension][lexical-react] Feature: HMR support for extensions#8959mayrang wants to merge 3 commits into
mayrang wants to merge 3 commits into
Conversation
Add HMRExtension to @lexical/extension that preserves editor state, editable flag, and undo/redo history across HMR cycles. The extension saves state to the bundler's HMR data store and restores it with prototype swaps when the new editor instance is created. Split non-component exports from LexicalTypeaheadMenuPlugin, LexicalAutoEmbedPlugin, and LexicalCollaborationContext into companion *Utils files for better Fast Refresh boundaries. Add FAQ documentation covering HMRExtension usage, Fast Refresh compatibility, and the @refresh reset fallback.
mayrang
requested review from
acywatson,
etrepum,
fantactuka,
ivailop7,
potatowagon and
zurfyx
as code owners
August 7, 2026 15:15
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Collaborator
|
examples can’t depend on new exports, we’d have to create a new dev-examples for this until this api is published |
examples/ depends on npm-published packages and can't import HMRExtension until the next release. Restore the inline implementation in the SvelteKit example and add a dev-examples/hmr app that imports HMRExtension from the workspace package.
Contributor
Author
|
Done — moved the example to |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Lexical relies on object identity for node class registration, command dispatch, and extension deduplication. When a bundler's HMR re-executes a module, those objects get recreated and the previous editor state is lost. This PR adds an
HMRExtensionto@lexical/extensionthat preserves editor state, the editable flag, and undo/redo history across HMR cycles. It also splits three@lexical/reactplugin modules for better React Fast Refresh compatibility.HMRExtension
On each HMR cycle, the extension saves the current
EditorState, editable flag, and (whenHistoryExtensionis present) the full undo/redo stacks to the bundler's HMR data store. When the new editor instance is created, it restores them — swappingObject.setPrototypeOfon every node in the saved state so they point at the new class prototypes.hot: HotContext | null— passimport.meta.hot(Vite, SvelteKit) ornullin production. TheHotContextinterface requires only{ readonly data: Record<string, unknown> }, so any bundler with a persistent data bag works.HistoryExtensionis a peer, undo/redo stacks are preserved automatically. The extension detects it at runtime viagetPeerDependencyFromEditor— no hard dependency.$initialEditorStategracefully.Fast Refresh splits
Vite's
react-refreshplugin applies state-preserving HMR only when a module exports nothing but React components. Three@lexical/reactplugins export hooks, classes, or commands alongside their component, which forces a full remount on every change.This PR extracts non-component exports into companion
*Utilsfiles:LexicalAutoEmbedPluginUtils.ts—AutoEmbedOption,EmbedConfig,INSERT_EMBED_COMMAND,URL_MATCHERLexicalCollaborationContextUtils.ts—CollaborationContext,useCollaborationContextLexicalTypeaheadMenuPluginUtils.ts—PUNCTUATION,useBasicTypeaheadTriggerMatch,SCROLL_TYPEAHEAD_OPTION_INTO_VIEW_COMMAND,getScrollParentBackwards compatibility
The original component modules re-export everything from their Utils counterpart, so existing import paths keep working. No API changes.
Design notes
HotContextinterface is deliberately minimal. Bundler HMR contexts have wildly different shapes (Vite hasaccept,dispose,prune; webpack hasmodule.hot.accept). The only shared property is a persistentdatabag. Widening this interface is possible if a use case comes up.*Utilssplit is one approach to Fast Refresh compatibility. An alternative is// @refresh resetdirectives on the component files, which forces a full remount but avoids the file split. The split gives more granular HMR boundaries — changes to the Utils file don't invalidate the component, and vice versa.HMRExtension.Test plan
HMRExtension.test.ts— content preservation, editable flag, undo/redo round-trip, multi-cycle, null hot, corrupted state, empty saved state, no-history peer.pnpm tsc --noEmit -p tsconfig.jsonclean.pnpm vitest run --project unit -t "HMRExtension"— 8 pass.