Skip to content

Commit

Permalink
[lexical-react] Bug Fix: Use useState(...)[0] instead of useMemo when…
Browse files Browse the repository at this point in the history
… creating LexicalEditor for React 19 StrictMode correctness
  • Loading branch information
etrepum committed May 6, 2024
1 parent c8e45fe commit 48ba6f4
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 107 deletions.
80 changes: 38 additions & 42 deletions packages/lexical-react/src/LexicalComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
*
*/

import type {LexicalComposerContextType} from '@lexical/react/LexicalComposerContext';
import type {
LexicalComposerContextType,
LexicalComposerContextWithEditor,
} from '@lexical/react/LexicalComposerContext';

import {
createLexicalComposerContext,
Expand All @@ -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';
Expand Down Expand Up @@ -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<LexicalComposerContextWithEditor>(() => {
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 (
<LexicalComposerContext.Provider value={composerContext}>
Expand Down
110 changes: 52 additions & 58 deletions packages/lexical-react/src/LexicalNestedComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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(
Expand Down Expand Up @@ -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<LexicalComposerContextWithEditor>(() => {
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();
Expand Down
3 changes: 0 additions & 3 deletions packages/lexical-react/src/shared/usePlainTextSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
3 changes: 0 additions & 3 deletions packages/lexical-react/src/shared/useRichTextSetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
2 changes: 1 addition & 1 deletion packages/lexical/src/LexicalEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 48ba6f4

Please sign in to comment.