On this sectionUsage

Usage

Mount the editor, seed it with initialValue, read changes with onChange, and render a saved document on the server without the editing surface.

Minimal usage

Render the block in a client component and give it a documentId. Without an adapter the document lives in component state only: it is there while the editor is mounted and gone after a reload.

tsx
"use client";

import { NotionLikeEditorBlock } from "@/components/blocks/notion-like-editor";

export function NotesEditor() {
  return <NotionLikeEditorBlock documentId="notes" />;
}

documentId

documentId is required and is the identity of the document. In persisted mode the editor is keyed by it: a new value unmounts the current editor and loads that document with adapter.load. Every document on a page needs its own id.

initialValue and onChange

initialValue is the content of a new document. The default is EMPTY_EDITOR_VALUE, one empty paragraph. With an adapter, the stored document wins as soon as it loads, and initialValue is used only when the adapter answers null.

onChange is called with the whole value after each change. It is a function prop, so the component that passes it must be a client component.

tsx
"use client";

import { useState } from "react";

import {
  NotionLikeEditorBlock,
  type EditorValue,
} from "@/components/blocks/notion-like-editor";

const START_VALUE: EditorValue = [
  { type: "h1", children: [{ text: "Release notes" }] },
  { type: "p", children: [{ text: "Write the summary of this release here." }] },
];

export function NotesEditor() {
  const [value, setValue] = useState<EditorValue>(START_VALUE);

  return (
    <div className="space-y-2">
      <NotionLikeEditorBlock documentId="notes" initialValue={START_VALUE} onChange={setValue} />
      <p className="text-muted-foreground text-xs">{value.length} top-level blocks</p>
    </div>
  );
}

readOnly

readOnly turns editing off. The content is still rendered, the document rules still apply, and the block handle is not shown. Use it for a viewer that still needs the editor's own markup, for example a comment you may edit in place later.

tsx
"use client";

import { NotionLikeEditorBlock } from "@/components/blocks/notion-like-editor";

export function NotesViewer() {
  return <NotionLikeEditorBlock documentId="notes" readOnly />;
}

Render saved content on the server

NotionLikeEditorStatic renders a stored value without Plate's editing surface. It has no hooks and no "use client" directive, so a server component can render it from a value read on the server. It takes a value and an optional className, and no adapters and no onChange.

tsx
import {
  NotionLikeEditorStatic,
  type EditorValue,
} from "@/components/blocks/notion-like-editor";

// No "use client" here: the static renderer has no hooks, so a server component can render it.
export function NotesView({ value }: { value: EditorValue }) {
  return (
    <div className="mx-auto w-full max-w-[var(--editor-max-width,700px)]">
      <NotionLikeEditorStatic value={value} />
    </div>
  );
}
The static renderer has no width of its own. Set one on the element around it, as the example does with max-w-[var(--editor-max-width,700px)], or the output will take the full width of its parent.

Next.js notes

The public entry point has no "use client" directive, so a server component can import from it. What needs the directive is the file that passes a function prop: onChange, onOpenPage, an adapter, or a provider. Put the editor and those props in one client component and render that component from your page.

  • NotionLikeEditorBlock is a client component in its own file. It can be rendered from a page, but only with serializable props.
  • NotionLikeEditorStatic is a server component in effect. Render it directly from a page.
  • In persisted mode the document is loaded in the browser, so the first render shows the editor's own loading skeleton.

Next steps