blokhaus

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

ParameterTypeDefaultDescription
debounceMsnumber300Debounce delay in milliseconds. Controls how often the serialized state is computed after editor changes.
onChange(serializedState: string) => voidundefinedCallback 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

FieldTypeDescription
serializedStatestringThe 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

components/StateViewer.tsx
"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:

components/AutoSave.tsx
"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:

app/editor/page.tsx
"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 editorState is serialized when the debounce timer fires. Intermediate states are discarded to minimize serialization overhead.
  • Stable callback reference: The onChange callback 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 editor or debounceMs changes.