Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 7 additions & 14 deletions packages/lexical-react/src/LexicalExtensionEditorComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@
import {getExtensionDependencyFromEditor} from '@lexical/extension';
import {ReactExtension} from '@lexical/react/ReactExtension';
import {LexicalEditorWithDispose} from 'lexical';
import {useEffect} from 'react';

export interface LexicalExtensionEditorComposerProps {
/**
* Your root extension, typically defined with {@link defineExtension}
* Your root extension, typically defined with {@link defineExtension}.
* The lifecycle of this editor is not owned by this component,
* you are responsible for calling `initialEditor.dispose()` if needed.
* Note also that any LexicalEditor can only be rendered to one root
* element, so if you try and use it from multiple components
* simultaneously then it will only be managed correctly by the last one
* to render.
*/
initialEditor: LexicalEditorWithDispose;
/**
Expand All @@ -33,18 +38,6 @@ export function LexicalExtensionEditorComposer({
initialEditor: editor,
children,
}: LexicalExtensionEditorComposerProps) {
useEffect(() => {
// Strict mode workaround
let didMount = false;
queueMicrotask(() => {
didMount = true;
});
return () => {
if (didMount) {
editor.dispose();
}
};
}, [editor]);
const {Component} = getExtensionDependencyFromEditor(
editor,
ReactExtension,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {LexicalExtensionComposer} from '@lexical/react/LexicalExtensionComposer'
import {LexicalExtensionEditorComposer} from '@lexical/react/LexicalExtensionEditorComposer';
import {RichTextPlugin} from '@lexical/react/LexicalRichTextPlugin';
import {ReactExtension} from '@lexical/react/ReactExtension';
import {ReactPluginHostExtension} from '@lexical/react/ReactPluginHostExtension';
import {ReactProviderExtension} from '@lexical/react/ReactProviderExtension';
import {toHaveNoViolations} from 'jest-axe';
import {
Expand All @@ -25,10 +26,13 @@ import {
$getState,
$getStateChange,
$setState,
COMMAND_PRIORITY_EDITOR,
createCommand,
createState,
DecoratorNode,
defineExtension,
EditorConfig,
LexicalCommand,
LexicalEditor,
LexicalEditorWithDispose,
StateConfigValue,
Expand Down Expand Up @@ -192,4 +196,93 @@ describe('LexicalExtensionEditorComposer', () => {
await Promise.resolve();
});
});

test('does not dispose the editor on unmount', async () => {
using editor = buildEditorFromExtensions({
dependencies: [ReactPluginHostExtension, RichTextPlugin],
name: '[root]',
});
const TestCommand: LexicalCommand<number> = createCommand('TestCommand');
const handled: number[] = [];
editor.registerCommand(
TestCommand,
(payload) => {
handled.push(payload);
return true;
},
COMMAND_PRIORITY_EDITOR,
);

await ReactTestUtils.act(async () => {
reactRoot.render(
<LexicalExtensionEditorComposer initialEditor={editor} />,
);
});
expect(editor.getRootElement()).not.toBe(null);
expect(editor.dispatchCommand(TestCommand, 1)).toBe(true);
expect(handled).toEqual([1]);

await ReactTestUtils.act(async () => {
reactRoot.render(null);
await Promise.resolve();
});

// The command registration must survive the composer unmount —
// LexicalExtensionEditorComposer no longer calls editor.dispose().
expect(editor.dispatchCommand(TestCommand, 2)).toBe(true);
expect(handled).toEqual([1, 2]);
});

test('can remount with the same editor after unmount', async () => {
using editor = buildEditorFromExtensions({
dependencies: [ReactPluginHostExtension, RichTextPlugin],
name: '[root]',
});
editor.update(
() =>
$getRoot()
.clear()
.append($createParagraphNode().append($createTextNode('nested'))),
{discrete: true},
);

// First mount
await ReactTestUtils.act(async () => {
reactRoot.render(
<LexicalExtensionEditorComposer initialEditor={editor} />,
);
});
expect(container?.textContent).toBe('nested');
const firstRoot = editor.getRootElement();
expect(firstRoot).not.toBe(null);

// Unmount
await ReactTestUtils.act(async () => {
reactRoot.render(null);
await Promise.resolve();
});

// Remount with the same editor instance — this mirrors the image-caption
// open/close/open cycle that previously broke because the editor was
// disposed on first unmount.
await ReactTestUtils.act(async () => {
reactRoot.render(
<LexicalExtensionEditorComposer initialEditor={editor} />,
);
});
expect(container?.textContent).toBe('nested');
expect(editor.getRootElement()).not.toBe(null);

// Editor still functions for updates after the remount.
await ReactTestUtils.act(async () => {
editor.update(
() =>
$getRoot()
.clear()
.append($createParagraphNode().append($createTextNode('updated'))),
{discrete: true},
);
});
expect(container?.textContent).toBe('updated');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ describe('useExtensionSignalValue', () => {
name: 'test',
});

const editor = buildEditorFromExtensions({
using editor = buildEditorFromExtensions({
dependencies: [ReactPluginHostExtension, TestExtension],
name: '[root]',
});
Expand Down Expand Up @@ -284,7 +284,7 @@ describe('useExtensionSignalValue', () => {
name: 'test',
});

const editor = buildEditorFromExtensions({
using editor = buildEditorFromExtensions({
dependencies: [ReactPluginHostExtension, TestExtension],
name: '[root]',
});
Expand Down Expand Up @@ -337,7 +337,7 @@ describe('useExtensionSignalValue', () => {
name: 'test',
});

const editor = buildEditorFromExtensions({
using editor = buildEditorFromExtensions({
dependencies: [ReactPluginHostExtension, TestExtension],
name: '[root]',
});
Expand Down Expand Up @@ -380,7 +380,7 @@ describe('useExtensionSignalValue', () => {
name: 'test',
});

const editor = buildEditorFromExtensions({
using editor = buildEditorFromExtensions({
dependencies: [ReactPluginHostExtension, TestExtension],
name: '[root]',
});
Expand Down
Loading