useEditorState
Subscribe to serialized editor state changes.
useEditorState is a React hook that subscribes to Lexical editor state changes and provides the serialized JSON state. It uses a debounced update listener to avoid performance issues on large documents.
Import
import { useEditorState } from "@blokhaus/core";Signature
function useEditorState(options?: {
debounceMs?: number;
onChange?: (serializedState: string) => void;
}): { serializedState: string };Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
debounceMs | number | 300 | Debounce delay in milliseconds. Controls how often the serialized state is computed after editor changes. |
onChange | (serializedState: string) => void | undefined | Callback invoked with the serialized JSON string after each debounced update. When provided, the hook skips React state updates for serializedState to avoid unnecessary re-renders. |
Return value
| Field | Type | Description |
|---|---|---|
serializedState | string | The editor state serialized as a JSON string via editorState.toJSON(). Empty string on initial render before the first update fires. |
Do not set debounceMs below 200. High-frequency serialization calls on
every keystroke degrade performance on large documents.
Requirements
This hook must be used inside an EditorRoot component. It calls useLexicalComposerContext() internally to access the Lexical editor instance.
Basic usage
Reading state for display
"use client";
import { useEditorState } from "@blokhaus/core";
function StateViewer() {
const { serializedState } = useEditorState();
return (
<pre className="text-xs mt-4 p-2 bg-gray-100 rounded overflow-auto max-h-60">
{serializedState}
</pre>
);
}Persisting to a backend
When you provide an onChange callback, the hook avoids triggering React re-renders for serializedState. This is the recommended pattern for persistence, since you typically do not need to render the raw JSON:
"use client";
import { useCallback } from "react";
import { useEditorState } from "@blokhaus/core";
function AutoSave() {
const handleChange = useCallback((json: string) => {
fetch("/api/save", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: json,
});
}, []);
useEditorState({ debounceMs: 500, onChange: handleChange });
return null; // Renders nothing
}Using with EditorRoot
The hook must be rendered as a child (or deeper descendant) of EditorRoot:
"use client";
import { EditorRoot, useEditorState } from "@blokhaus/core";
function StatePersistence() {
useEditorState({
onChange: (json) => localStorage.setItem("editor-state", json),
});
return null;
}
export default function EditorPage() {
const savedState =
typeof window !== "undefined" ? localStorage.getItem("editor-state") : null;
return (
<EditorRoot
namespace="my-editor"
initialState={savedState}
className="min-h-[400px] p-4 border rounded"
>
<StatePersistence />
</EditorRoot>
);
}Implementation details
The hook works by registering a Lexical update listener via editor.registerUpdateListener(). Key implementation details:
- Debouncing: Only the most recent
editorStateis serialized when the debounce timer fires. Intermediate states are discarded to minimize serialization overhead. - Stable callback reference: The
onChangecallback is stored in a ref (useRef) so changing the callback does not re-register the update listener. - Cleanup: The update listener and any pending timeout are cleaned up on unmount.
- Dependency tracking: The effect re-runs only when
editorordebounceMschanges.
Related
- EditorRoot -- The parent component that provides the Lexical context
- useWordCount -- Hook for word and character counts
- Serialization guide -- Guide to saving and restoring editor state