With Lexical, how do I set default initial text?

Viewed 1323

With the Lexical text editor framework, what's the easiest way to initialize the editor in React with a default text string?

I could, for instance, create an instance, manually save the JSON state, and then copy that JSON blob to my code, to set as initialEditorState to PlainTextPlugin, but this seems like I have to be missing something.

Thanks

2 Answers

Your intuition is correct. Avoid touching the EditorState directly, even when it's serialized as a JSON. Manipulating internals (inc. node private properties) can potentially lead to unexpected behavior/errors in future releases.

The initialEditorState can take many shapes:

export type InitialEditorStateType = null | string | EditorState | (() => void);

  • null -> empty EditorState (just a RootNode)
  • string -> a JSON stringified EditorState. Behind the scenes it calls JSON.parse(editor.setEditorState)
  • EditorState -> an EditorState. Behind the scenes it calls - editor.setEditorState() (the undo-redo/History plugin uses this) (() => void) -> an editor.update function

The one you're interested in is (() => void) -> an editor update.

You can run an editor.update like follows:

<LexicalPlainTextPlugin initialEditorState={() => {
  const paragraph = $createParagraphNode();
  const text = $createTextNode('foo');
  paragraph.append(text);
  $getRoot().append(paragraph);
  root.selectEnd();
}} />

No need to cache (useCallback) initialEditorState as it's only processed once

Side note: we're planning to move initialEditorState (that currently lives in LexicalPlainTextPlugin and LexicalRichTextPlugin) to LexicalComposer but it will work the same way.


We recommend avoiding manually handcrafted solutions too:

// PrepopulatePlugin.js
useLayoutEffect(() => {
  editor.update(() => {
    // Prepopulate
  });
}, [editor]);

We built LexicalContentEditable to work well with SSR and handle the contenteditable appropriately. If you were to build your own custom solution you would have to repeat this process.

The way to achieve it found in lexical codebase is like so

//...
<RichTextPlugin
   initialEditorState={ prepopulatedRichText}/>
//...

function prepopulatedRichText() {
 const root = $getRoot();
 if (root.getFirstChild() === null) {
   const heading = $createHeadingNode('h1');
   heading.append($createTextNode('Welcome to the playground'));
   root.append(heading);
   const quote = $createQuoteNode();
   quote.append(
     $createTextNode(
       `In case you were wondering what the black box at the bottom is – it's the debug view, showing the current state of editor. ` +
         `You can hide it by pressing on the settings control in the bottom-right of your screen and toggling the debug view setting.`,
     ),
   );
   root.append(quote);
   const paragraph = $createParagraphNode();
   paragraph.append(
     $createTextNode('The playground is a demo environment built with '),
     $createTextNode('@lexical/react').toggleFormat('code'),
     $createTextNode('.'),
     $createTextNode(' Try typing in '),
     $createTextNode('some text').toggleFormat('bold'),
     $createTextNode(' with '),
     $createTextNode('different').toggleFormat('italic'),
     $createTextNode(' formats.'),
   );
   root.append(paragraph);
   const paragraph2 = $createParagraphNode();
   paragraph2.append(
     $createTextNode(
       'Make sure to check out the various plugins in the toolbar. You can also use #hashtags or @-mentions too!',
     ),
   );
   root.append(paragraph2);
   const paragraph3 = $createParagraphNode();
   paragraph3.append(
     $createTextNode(`If you'd like to find out more about Lexical, you can:`),
   );
   root.append(paragraph3);
   const list = $createListNode('bullet');
   list.append(
     $createListItemNode().append(
       $createTextNode(`Visit the `),
       $createLinkNode('https://lexical.dev/').append(
         $createTextNode('Lexical website'),
       ),
       $createTextNode(` for documentation and more information.`),
     ),
     $createListItemNode().append(
       $createTextNode(`Check out the code on our `),
       $createLinkNode('https://github.com/facebook/lexical').append(
         $createTextNode('GitHub repository'),
       ),
       $createTextNode(`.`),
     ),
     $createListItemNode().append(
       $createTextNode(`Playground code can be found `),
       $createLinkNode(
         'https://github.com/facebook/lexical/tree/main/packages/lexical-playground',
       ).append($createTextNode('here')),
       $createTextNode(`.`),
     ),
     $createListItemNode().append(
       $createTextNode(`Join our `),
       $createLinkNode('https://discord.com/invite/KmG4wQnnD9').append(
         $createTextNode('Discord Server'),
       ),
       $createTextNode(` and chat with the team.`),
     ),
   );
   root.append(list);
   const paragraph4 = $createParagraphNode();
   paragraph4.append(
     $createTextNode(
       `Lastly, we're constantly adding cool new features to this playground. So make sure you check back here when you next get a chance :).`,
     ),
   );
   root.append(paragraph4);
 }
}

but I prefer to use stringified editorState then parse it to editorState by parseEditorState function owned by instance.

Here is another problem I'm confused which also seems I have to be missed something.

ParseEditorState is a function only used for instance, but I can't use useLexicalComposerContext under the init component which will return LexicalComposer(unexpected error will cause), so I need to write an extra plugin to achieve it. Something like that:

import { useSelector } from "react-redux";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { useEffect } from "react";

export default function EditorStatePlugin() {
  const activeNote = useSelector((state) => state.note.activeNote);
  const [editor] = useLexicalComposerContext();
  const state = editor.parseEditorState(
    activeNote?.content ||
      '{"_nodeMap":[["root",{"__children":["1"],"__dir":null,"__format":0,"__indent":0,"__key":"root","__parent":null,"__type":"root"}],["1",{"__type":"paragraph","__parent":"root","__key":"1","__children":[],"__format":0,"__indent":0,"__dir":null}]],"_selection":{"anchor":{"key":"1","offset":0,"type":"element"},"focus":{"key":"1","offset":0,"type":"element"},"type":"range"}}'
  );
  useEffect(() => {
    editor.setEditorState(state);
  }, [activeNote]);

  return null;
}

It seems not a good way to write, is there a better way to do it?

Related