Setting Initial State in Lexical Text Editor
What is Lexical?
TL;DR: Do you want to build something like Notion’s text editor? Then Lexical is probably the tool for you.
Lexical is a powerful, extensible text editor framework developed by Facebook (now Meta). It’s designed to be flexible, performant, and accessible, making it an excellent choice for a wide range of applications. Lexical can be used to build simple text inputs, complex document editors, or even collaborative editing systems.
Setting the initial state
I found out that the best way to learn about Lexical is to analyze the types of the component. For the initial config for example we have:
export type InitialEditorStateType =
| null
| string
| EditorState
| ((editor: LexicalEditor) => void);
export type InitialConfigType = Readonly<{
editor__DEPRECATED?: LexicalEditor | null;
namespace: string;
nodes?: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement>;
onError: (error: Error, editor: LexicalEditor) => void;
editable?: boolean;
theme?: EditorThemeClasses;
editorState?: InitialEditorStateType;
html?: HTMLConfig;
}>;
Do you see that editorState
with the InitialEditorStateType
type? That’s the place where we can set the initial state of the editor.
As you can see, it can be a string
, EditorState
, null
, or a function that returns a void
.
Note that string is not a simple string! It’s a JSON stringified EditorState. Behind the scenes it calls JSON.parse(editor.setEditorState)
.
In my case I’m building a WhatsApp template builder with Lexical, and users can choose from a list of pre-built templates.
So basically:
- I have an array of objects where each object has a
bodyContent
property. - I want to set the initial state of the editor to the
bodyContent
of the selected template. - When users click on a template I use query parameters to set the template id.
- I can then use the
useSearchParams
hook from Remix to get the template id and set the initial state of the editor. - My
bodyContent
is something like this:
In {{1}}, our birthday unfolds,\n\n
A thank you for trust you hold.\n\n
A special surprise awaits,\n\n
Just for you, behind these gates.\n\n
Click below, see deals so rare,\n\n
But hurry, time won't spare.\n\n
Limited offers tick away,\n\n
Seize this gift without delay.
- And I created this function to convert the
bodyContent
to anEditorState
(i.e a function that imperatively populates editor):
export function createInitialConfig(
template?: string | null
): InitialConfigType {
const baseConfig: Omit<InitialConfigType, "editorState"> = {
namespace: "whatsapp-template-editor",
theme: whatsappTemplateEditorTheme,
onError(error: any) {
throw error;
},
nodes: [],
editable: true,
};
if (template) {
return {
...baseConfig,
editorState: () => {
const root = $getRoot();
if (root.getFirstChild() === null) {
const paragraph = $createParagraphNode();
const parts = template.split(/(\{\{.*?\}\}|\*.*?\*)/);
parts.forEach((part) => {
if (part.startsWith("{{") && part.endsWith("}}")) {
// Placeholder, create as code
paragraph.append(createFormattedText(part, ["code"]));
} else if (part.startsWith("*") && part.endsWith("*")) {
// Bold text
paragraph.append(
createFormattedText(part.slice(1, -1), ["bold"])
);
} else {
// Regular text
paragraph.append($createTextNode(part));
}
});
root.append(paragraph);
}
},
};
} else {
return baseConfig;
}
}
function createFormattedText(
text: string,
formats: TextFormatType[] = []
): LexicalNode {
let node = $createTextNode(text);
formats.forEach((format) => {
node = node.toggleFormat(format);
});
return node;
}
And this is how I set up my initial state:
<LexicalComposer initialConfig={createInitialConfig(initialValue)}>
...
</LexicalComposer>
What to avoid when using Lexical:
- Avoid touching the EditorState directly, even when it’s serialized as a JSON.