/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * */ import { getExtensionDependencyFromEditor, LexicalBuilder, } from '@lexical/extension'; import {ReactConfig, ReactExtension} from '@lexical/react/ReactExtension'; import {ReactProviderExtension} from '@lexical/react/ReactProviderExtension'; import {type AnyLexicalExtensionArgument, configExtension} from 'lexical'; import {useEffect, useMemo} from 'react'; export interface LexicalExtensionComposerProps { /** * Your root extension, typically defined with {@link defineExtension} */ extension: AnyLexicalExtensionArgument; /** * Any children will have access to useLexicalComposerContext (e.g. for React plug-ins or UX) */ children?: React.ReactNode; /** * Override the default ContentEditable that is rendered as the first child of the * composer. If this is null, then it is your responsibility to render a ContentEditable * elsewhere in the tree. This is equivalent to * `configExtension(ReactExtension, {contentEditable})` in your extension dependencies. */ contentEditable?: ReactConfig['contentEditable']; } /** * The equivalent of LexicalComposer for an extension. Make sure that your extension * argument is stable (e.g. using module scope or useMemo) so * that you are not re-creating the editor on every render! * * @example * Module scoped extension * ```tsx * const extension = defineExtension({ * name: "[root]", * dependencies: [RichTextExtension, HistoryExtension, EmojiExtension] * }); * function MyEditor({ children }) { * return ({children}); * } * ``` * * @example * useMemo extension * ```tsx * function MyEditor({ emojiBaseUrl, children }) { * const extension = useMemo(() => { * return defineExtension({ * name: "[root]", * dependencies: [ * RichTextExtension, * HistoryExtension, * configExtension(EmojiExtension, { emojiBaseUrl }), * ], * }); * }, [emojiBaseUrl]); * return ({children}); * } * ``` * * @example * Incorrect usage with unstable extension * ```tsx * function MyBrokenEditor({ emojiBaseUrl }) { * // This argument is not stable, the editor is re-created every render and * // all state is lost! * const extension = defineExtension({ * name: "[root]", * dependencies: [RichTextExtension, HistoryExtension, EmojiExtension] * }); * return ({children}); * } * ``` */ export function LexicalExtensionComposer({ extension, children, contentEditable, }: LexicalExtensionComposerProps) { const editor = useMemo(() => { const builder = LexicalBuilder.fromExtensions([ ReactProviderExtension, configExtension( ReactExtension, contentEditable === undefined ? {} : {contentEditable}, ), extension, ]); return builder.buildEditor(); }, [contentEditable, extension]); useEffect(() => { // Strict mode workaround let didMount = false; queueMicrotask(() => { didMount = true; }); return () => { if (didMount) { editor.dispose(); } }; }, [editor]); const {Component} = getExtensionDependencyFromEditor( editor, ReactExtension, ).output; return {children}; }