How to save and load editor state from localStorage with React? #1937
-
|
From the examples: const initialEditorState = await loadContent();
const editorStateRef = useRef();
<LexicalComposer ...>
<LexicalRichTextPlugin initialEditorState={initialEditorState} />
<LexicalOnChangePlugin onChange={editorState => editorStateRef.current = editorState} />
<Button label="Save" onPress={() => {
if (editorStateRef.current) {
saveContent(editorStateRef.current.toJSON())
}
}} />
</LexicalComposer>This makes sense, except for how I'd like to pass I would've expected What's the best way to solve this problem at the moment? Thanks 🙏 |
Beta Was this translation helpful? Give feedback.
Replies: 9 comments 12 replies
-
|
It's not obvious but
Does passing the JSON.stringifed version work for you? |
Beta Was this translation helpful? Give feedback.
-
|
This is what I ended up going with. I'd love for it to work on first render, though I couldn't seem to get that working. import { useLocalStorage } from 'react-use'
import OnChangePlugin from '@lexical/react/LexicalOnChangePlugin'
function RestoreFromLocalStoragePlugin() {
const [editor] = useLexicalComposerContext()
const [serializedEditorState, setSerializedEditorState] = useLocalStorage<
string | null
>('my-editor-state-example-key', null)
const [isFirstRender, setIsFirstRender] = React.useState(true)
React.useEffect(() => {
if (isFirstRender) {
setIsFirstRender(false)
if (serializedEditorState) {
const initialEditorState = editor.parseEditorState(serializedEditorState)
editor.setEditorState(initialEditorState)
}
}
}, [isFirstRender, serializedEditorState, editor])
const onChange = React.useCallback(
(editorState: EditorState) => {
setSerializedEditorState(JSON.stringify(editorState.toJSON()))
},
[setSerializedEditorState]
)
// TODO: add ignoreSelectionChange
return <OnChangePlugin onChange={onChange} />
} |
Beta Was this translation helpful? Give feedback.
-
|
easier if just make another plugins I think the concept similar like make readOnly attach onChange with handlers |
Beta Was this translation helpful? Give feedback.
-
|
The const loadContent = async () => {
// read from database, local storage, etc.
// 'empty' editor
const value = '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}';
return value;
}
const initialEditorState = await loadContent();
const initialConfig = {
namespace: 'Editor',
theme: {},
onError: (error) => console.log(error),
editorState: initialEditorState,
};
<LexicalComposer initialConfig={initialConfig}>
<LexicalRichTextPlugin
contentEditable={<LexicalContentEditable />}
placeholder=""
/>
<LexicalOnChangePlugin
onChange={editorState => {
editorState.read(() => {
// write to database, local storage, etc.
const value = JSON.stringify(editorState); // or JSON.stringify(editorState.toJSON())
console.log(value);
});
}}
/>
</LexicalComposer> |
Beta Was this translation helpful? Give feedback.
-
|
If you need to avoid the onChange handler of being triggered initially this is possible by explicitly setting the Using const editorConfig = {
namespace: 'my-editor',
theme: DefaultTheme,
onError(error: Error) {
throw error;
},
editorState: null, <---
nodes: [
HeadingNode,
ImageNode,
ListNode,
ListItemNode,
QuoteNode,
CodeNode,
CodeHighlightNode,
TableNode,
TableCellNode,
TableRowNode,
AutoLinkNode,
LinkNode,
HorizontalRuleNode
]
};
...
<LexicalComposer initialConfig={editorConfig}>
...
</LexicalComposer> |
Beta Was this translation helpful? Give feedback.
-
|
didn't read. this worked for me in Next.js function LocalStoragePlugin() {
const [editor] = useLexicalComposerContext()
useEffect(() => {
const state = editor.parseEditorState(
localStorage?.editor || editor.getEditorState()
)
editor.setEditorState(state)
}, [editor])
return null
} |
Beta Was this translation helpful? Give feedback.
-
|
In React using TypeScript, getting the Variable declaration -> Then I was able to pass it into the config object like so. const initialConfig = {
namespace: 'EditorWidget',
theme,
onError,
nodes: [
HeadingNode
],
editorState: initialEditorState,
}; |
Beta Was this translation helpful? Give feedback.
-
|
Just roll your own composer, it's simple. The default one sometimes does not get executed then you get an empty state: I have to replace the default import type { InitialConfigType } from '@lexical/react/LexicalComposer'
import type { LexicalComposerContextType } from '@lexical/react/LexicalComposerContext'
import {
LexicalComposerContext,
createLexicalComposerContext
} from '@lexical/react/LexicalComposerContext'
import { type LexicalEditor, createEditor } from 'lexical'
import type * as React from 'react'
import { useLayoutEffect, useMemo } from 'react'
export type SimpleInitialConfigType = Omit<InitialConfigType, 'editorState'> & {
initialState: string | ((editor: LexicalEditor) => void)
}
type SimpleLexicalComposerProps = React.PropsWithChildren<{
initialConfig: SimpleInitialConfigType
}>
export function SimpleLexicalComposer({
initialConfig,
children
}: SimpleLexicalComposerProps): JSX.Element {
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
const composerContext: [LexicalEditor, LexicalComposerContextType] = useMemo(
() => {
const { theme, namespace, nodes, onError, initialState, html } = initialConfig
const context: LexicalComposerContextType = createLexicalComposerContext(null, theme)
const editor = createEditor({
editable: initialConfig.editable,
html,
namespace,
nodes,
onError: (error) => onError(error, editor),
theme
})
// Any initialization put here
initializeEditor(editor, initialState)
return [editor, context]
},
// We only do this for init
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
)
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
useLayoutEffect(() => {
const isEditable = initialConfig.editable
const [editor] = composerContext
editor.setEditable(isEditable !== undefined ? isEditable : true)
// We only do this for init
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
return (
<LexicalComposerContext.Provider value={composerContext}>
{children}
</LexicalComposerContext.Provider>
)
}
const HISTORY_MERGE_OPTIONS = { tag: 'history-merge' }
function initializeEditor(
editor: LexicalEditor,
initialEditorState: SimpleInitialConfigType['initialState']
): void {
switch (typeof initialEditorState) {
case 'string': {
const parsedEditorState = editor.parseEditorState(initialEditorState)
editor.setEditorState(parsedEditorState, HISTORY_MERGE_OPTIONS)
break
}
case 'function': {
// Roll here any initialization
initialEditorState(editor)
break
}
default: {
console.error('Invalid initial state', initialEditorState)
}
}
}Also the prop name |
Beta Was this translation helpful? Give feedback.
-
|
I am trying to create a LexicalViewer so that once I will pass any stored content by Lexical editor in JSON format, it should be rendered properly. Some how I can see the data in the console but in the UI its not showing anything. Any suggestion ? |
Beta Was this translation helpful? Give feedback.
This is what I ended up going with. I'd love for it to work on first render, though I couldn't seem to get that working.