From 48ba6f4dcac4bd2b7315935bac4cf9e915bfd6ba Mon Sep 17 00:00:00 2001 From: Bob Ippolito Date: Mon, 6 May 2024 13:41:24 -0700 Subject: [PATCH] [lexical-react] Bug Fix: Use useState(...)[0] instead of useMemo when creating LexicalEditor for React 19 StrictMode correctness --- .../lexical-react/src/LexicalComposer.tsx | 80 ++++++------- .../src/LexicalNestedComposer.tsx | 110 +++++++++--------- .../src/shared/usePlainTextSetup.ts | 3 - .../src/shared/useRichTextSetup.ts | 3 - packages/lexical/src/LexicalEditor.ts | 2 +- 5 files changed, 91 insertions(+), 107 deletions(-) diff --git a/packages/lexical-react/src/LexicalComposer.tsx b/packages/lexical-react/src/LexicalComposer.tsx index c40568b7d6d..ece0c44b926 100644 --- a/packages/lexical-react/src/LexicalComposer.tsx +++ b/packages/lexical-react/src/LexicalComposer.tsx @@ -6,7 +6,10 @@ * */ -import type {LexicalComposerContextType} from '@lexical/react/LexicalComposerContext'; +import type { + LexicalComposerContextType, + LexicalComposerContextWithEditor, +} from '@lexical/react/LexicalComposerContext'; import { createLexicalComposerContext, @@ -25,7 +28,7 @@ import { LexicalNode, LexicalNodeReplacement, } from 'lexical'; -import {useMemo} from 'react'; +import {useRef, useState} from 'react'; import * as React from 'react'; import {CAN_USE_DOM} from 'shared/canUseDOM'; import useLayoutEffect from 'shared/useLayoutEffect'; @@ -54,55 +57,48 @@ type Props = React.PropsWithChildren<{ }>; export function LexicalComposer({initialConfig, children}: Props): JSX.Element { - const composerContext: [LexicalEditor, LexicalComposerContextType] = useMemo( - () => { - const { - theme, + const initialConfigRef = useRef(initialConfig); + const composerContext = useState(() => { + const { + theme, + namespace, + editable, + editor__DEPRECATED: initialEditor, + nodes, + onError, + editorState: initialEditorState, + html, + } = initialConfigRef.current; + + const context: LexicalComposerContextType = createLexicalComposerContext( + null, + theme, + ); + + let editor = initialEditor || null; + + if (editor === null) { + const newEditor = createEditor({ + editable, + html, namespace, - editor__DEPRECATED: initialEditor, nodes, - onError, - editorState: initialEditorState, - html, - } = initialConfig; - - const context: LexicalComposerContextType = createLexicalComposerContext( - null, + onError: (error) => onError(error, newEditor), theme, - ); - - let editor = initialEditor || null; + }); + initializeEditor(newEditor, initialEditorState); - if (editor === null) { - const newEditor = createEditor({ - editable: initialConfig.editable, - html, - namespace, - nodes, - onError: (error) => onError(error, newEditor), - theme, - }); - initializeEditor(newEditor, initialEditorState); - - editor = newEditor; - } - - return [editor, context]; - }, + editor = newEditor; + } - // We only do this for init - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ); + return [editor, context]; + })[0]; useLayoutEffect(() => { - const isEditable = initialConfig.editable; + const isEditable = initialConfigRef.current.editable; const [editor] = composerContext; editor.setEditable(isEditable !== undefined ? isEditable : true); - - // We only do this for init - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [composerContext]); return ( diff --git a/packages/lexical-react/src/LexicalNestedComposer.tsx b/packages/lexical-react/src/LexicalNestedComposer.tsx index 8aa9326e429..6cf1675a93c 100644 --- a/packages/lexical-react/src/LexicalNestedComposer.tsx +++ b/packages/lexical-react/src/LexicalNestedComposer.tsx @@ -6,7 +6,10 @@ * */ -import type {LexicalComposerContextType} from '@lexical/react/LexicalComposerContext'; +import type { + LexicalComposerContextType, + LexicalComposerContextWithEditor, +} from '@lexical/react/LexicalComposerContext'; import type {KlassConstructor, Transform} from 'lexical'; import {useCollaborationContext} from '@lexical/react/LexicalCollaborationContext'; @@ -21,8 +24,7 @@ import { LexicalNode, LexicalNodeReplacement, } from 'lexical'; -import * as React from 'react'; -import {ReactNode, useContext, useEffect, useMemo, useRef} from 'react'; +import {ReactNode, useContext, useEffect, useRef, useState} from 'react'; import invariant from 'shared/invariant'; function getTransformSetFromKlass( @@ -56,69 +58,61 @@ export function LexicalNestedComposer({ const [parentEditor, {getTheme: getParentTheme}] = parentContext; - const composerContext: [LexicalEditor, LexicalComposerContextType] = useMemo( - () => { - const composerTheme: EditorThemeClasses | undefined = - initialTheme || getParentTheme() || undefined; + const composerContext = useState(() => { + const composerTheme: EditorThemeClasses | undefined = + initialTheme || getParentTheme() || undefined; - const context: LexicalComposerContextType = createLexicalComposerContext( - parentContext, - composerTheme, - ); + const context: LexicalComposerContextType = createLexicalComposerContext( + parentContext, + composerTheme, + ); - if (composerTheme !== undefined) { - initialEditor._config.theme = composerTheme; - } + if (composerTheme !== undefined) { + initialEditor._config.theme = composerTheme; + } - initialEditor._parentEditor = parentEditor; - - if (!initialNodes) { - const parentNodes = (initialEditor._nodes = new Map( - parentEditor._nodes, - )); - for (const [type, entry] of parentNodes) { - initialEditor._nodes.set(type, { - exportDOM: entry.exportDOM, - klass: entry.klass, - replace: entry.replace, - replaceWithKlass: entry.replaceWithKlass, - transforms: getTransformSetFromKlass(entry.klass), - }); - } - } else { - for (let klass of initialNodes) { - let replace = null; - let replaceWithKlass = null; - - if (typeof klass !== 'function') { - const options = klass; - klass = options.replace; - replace = options.with; - replaceWithKlass = options.withKlass || null; - } - const registeredKlass = initialEditor._nodes.get(klass.getType()); - - initialEditor._nodes.set(klass.getType(), { - exportDOM: registeredKlass ? registeredKlass.exportDOM : undefined, - klass, - replace, - replaceWithKlass, - transforms: getTransformSetFromKlass(klass), - }); + initialEditor._parentEditor = parentEditor; + + if (!initialNodes) { + const parentNodes = (initialEditor._nodes = new Map(parentEditor._nodes)); + for (const [type, entry] of parentNodes) { + initialEditor._nodes.set(type, { + exportDOM: entry.exportDOM, + klass: entry.klass, + replace: entry.replace, + replaceWithKlass: entry.replaceWithKlass, + transforms: getTransformSetFromKlass(entry.klass), + }); + } + } else { + for (let klass of initialNodes) { + let replace = null; + let replaceWithKlass = null; + + if (typeof klass !== 'function') { + const options = klass; + klass = options.replace; + replace = options.with; + replaceWithKlass = options.withKlass || null; } + const registeredKlass = initialEditor._nodes.get(klass.getType()); + + initialEditor._nodes.set(klass.getType(), { + exportDOM: registeredKlass ? registeredKlass.exportDOM : undefined, + klass, + replace, + replaceWithKlass, + transforms: getTransformSetFromKlass(klass), + }); } + } - initialEditor._config.namespace = parentEditor._config.namespace; - - initialEditor._editable = parentEditor._editable; + initialEditor._config.namespace = parentEditor._config.namespace; - return [initialEditor, context]; - }, + initialEditor._editable = parentEditor._editable; - // We only do this for init - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ); + return [initialEditor, context]; + })[0]; // If collaboration is enabled, make sure we don't render the children until the collaboration subdocument is ready. const {isCollabActive, yjsDocMap} = useCollaborationContext(); diff --git a/packages/lexical-react/src/shared/usePlainTextSetup.ts b/packages/lexical-react/src/shared/usePlainTextSetup.ts index d02b61084b1..9af3eef6ed9 100644 --- a/packages/lexical-react/src/shared/usePlainTextSetup.ts +++ b/packages/lexical-react/src/shared/usePlainTextSetup.ts @@ -19,8 +19,5 @@ export function usePlainTextSetup(editor: LexicalEditor): void { registerPlainText(editor), registerDragonSupport(editor), ); - - // We only do this for init - // eslint-disable-next-line react-hooks/exhaustive-deps }, [editor]); } diff --git a/packages/lexical-react/src/shared/useRichTextSetup.ts b/packages/lexical-react/src/shared/useRichTextSetup.ts index 45972eeb217..99d8e70eb5c 100644 --- a/packages/lexical-react/src/shared/useRichTextSetup.ts +++ b/packages/lexical-react/src/shared/useRichTextSetup.ts @@ -19,8 +19,5 @@ export function useRichTextSetup(editor: LexicalEditor): void { registerRichText(editor), registerDragonSupport(editor), ); - - // We only do this for init - // eslint-disable-next-line react-hooks/exhaustive-deps }, [editor]); } diff --git a/packages/lexical/src/LexicalEditor.ts b/packages/lexical/src/LexicalEditor.ts index 468cfbd413a..98821fcf479 100644 --- a/packages/lexical/src/LexicalEditor.ts +++ b/packages/lexical/src/LexicalEditor.ts @@ -447,7 +447,7 @@ export function createEditor(editorConfig?: CreateEditorArgs): LexicalEditor { // Ensure custom nodes implement required methods. if (__DEV__) { const name = klass.name; - if (name !== 'RootNode') { + if (name !== 'RootNode' && name !== 'ArtificialNode__DO_NOT_USE') { const proto = klass.prototype; ['getType', 'clone'].forEach((method) => { // eslint-disable-next-line no-prototype-builtins