Skip to content

React

React 19 components and hooks for Editu editor.

Installation

bash
npm install @editu/react
# or
pnpm add @editu/react
# or
yarn add @editu/react

Requirements

  • React 19
  • React DOM 19

Quick Start

Use the Editu component:

tsx
import { Editu } from '@editu/react';
import '@editu/core/styles.css';

function App() {
  return (
    <Editu
      placeholder="Type '/' for commands..."
      onUpdate={({ editor }) => console.log(editor.getJSON())}
    />
  );
}

Advanced Setup

To customize, use individual components with hooks:

tsx
import { EdituEditor, EdituBubbleMenu, useEdituEditor } from '@editu/react';
import '@editu/core/styles.css';

function Editor() {
  const editor = useEdituEditor({
    placeholder: "Type '/' for commands...",
  });

  return (
    <div className="editor-container">
      <EdituEditor editor={editor} />
      {editor && <EdituBubbleMenu editor={editor} />}
    </div>
  );
}

Components

Editu

All-in-one editor component with built-in bubble menu.

tsx
import { Editu } from '@editu/react';

<Editu
  initialContent={{ type: 'doc', content: [] }}
  placeholder="Start writing..."
  editable={true}
  autofocus="end"
  showBubbleMenu={true}
  enableEmbed={true}
  className="my-editor"
  features={{
    image: { onUpload: async (file) => 'url' },
  }}
  onUpdate={({ editor }) => {}}
  onCreate={({ editor }) => {}}
  onFocus={({ editor }) => {}}
  onBlur={({ editor }) => {}}
/>

Props

PropTypeDefaultDescription
initialContentJSONContent-Initial content (JSON)
initialMarkdownstring-Initial content (Markdown)
placeholderstring-Placeholder text
editablebooleantrueEditable state
autofocusboolean | 'start' | 'end' | 'all' | number-Auto focus
featuresEdituFeatureOptions-Feature options
classNamestring-CSS class
showToolbarbooleanfalseShow fixed toolbar above editor
showBubbleMenubooleantrueShow bubble menu
enableEmbedboolean-Enable embed in links
extensionsExtensions-Additional Tiptap extensions
transformDiagramsOnImportbooleantrueTransform diagram code blocks on import
onUpdateFunction-Update callback
onCreateFunction-Create callback
onDestroyFunction-Destroy callback
onSelectionUpdateFunction-Selection change callback
onFocusFunction-Focus callback
onBlurFunction-Blur callback

Hooks

useEdituEditor

This hook creates and manages a Editu editor instance.

tsx
import { useEdituEditor } from '@editu/react';

function Editor() {
  const editor = useEdituEditor({
    initialContent: { type: 'doc', content: [] },
    placeholder: 'Start writing...',
    features: {
      markdown: true,
      mathematics: true,
    },
    onUpdate: ({ editor }) => {
      console.log(editor.getJSON());
    },
  });

  return <EdituEditor editor={editor} />;
}

Options

See Configuration for full options.

Return Value

Returns Editor | null. The editor instance starts as null during SSR and before initialization.

useEdituState

This hook forces a component re-render on editor state changes.

tsx
import { useEdituState } from '@editu/react';

function EditorStats({ editor }) {
  // Re-renders when editor state changes
  useEdituState(() => editor);

  if (!editor) return null;

  return (
    <div>
      <span>{editor.storage.characterCount?.characters() ?? 0} characters</span>
      <span>{editor.storage.characterCount?.words() ?? 0} words</span>
      <span>{editor.isFocused ? 'Focused' : 'Blurred'}</span>
    </div>
  );
}

useEdituEditorState

This hook returns computed editor state that updates reactively. It provides commonly needed properties like character count, word count, and undo/redo availability.

tsx
import { useEdituEditor, useEdituEditorState, EdituEditor } from '@editu/react';

function Editor() {
  const editor = useEdituEditor();
  const { characterCount, wordCount, canUndo, canRedo, isFocused, isEmpty } =
    useEdituEditorState(() => editor);

  return (
    <div>
      <EdituEditor editor={editor} />
      <div className="status-bar">
        <span>{characterCount} characters</span>
        <span>{wordCount} words</span>
      </div>
    </div>
  );
}

Return Value

Returns EdituEditorState:

PropertyTypeDescription
isFocusedbooleanWhether the editor is focused
isEmptybooleanWhether the editor is empty
canUndobooleanWhether undo is available
canRedobooleanWhether redo is available
characterCountnumberCharacter count
wordCountnumberWord count

useEdituAutoSave

This hook automatically saves editor content.

tsx
import { useEdituAutoSave, EdituEditor, EdituSaveIndicator } from '@editu/react';

function Editor() {
  const editor = useEdituEditor();

  const { status, lastSaved, save, restore } = useEdituAutoSave(() => editor, {
    debounceMs: 2000,
    storage: 'localStorage',
    key: 'my-editor-content',
    onSave: (content) => console.log('Saved'),
    onError: (error) => console.error('Save failed', error),
  });

  return (
    <div>
      <EdituEditor editor={editor} />
      <EdituSaveIndicator status={status} lastSaved={lastSaved} />
    </div>
  );
}

useEdituMarkdown

This hook provides two-way Markdown synchronization with debouncing.

tsx
import { useEdituEditor, useEdituMarkdown, EdituEditor } from '@editu/react';

function MarkdownEditor() {
  const editor = useEdituEditor();
  const { markdown, setMarkdown, isPending } = useEdituMarkdown(() => editor, {
    debounceMs: 300, // default: 300ms
  });

  return (
    <div>
      <EdituEditor editor={editor} />
      <textarea 
        value={markdown} 
        onChange={(e) => setMarkdown(e.target.value)}
      />
      {isPending && <span>Syncing...</span>}
    </div>
  );
}

Return Value

PropertyTypeDescription
markdownstringCurrent Markdown content
setMarkdown(md: string) => voidUpdate editor from Markdown
isPendingbooleanWhether sync is pending

useEdituTheme

This hook accesses theme state within EdituThemeProvider.

tsx
import { useEdituTheme, EdituThemeProvider } from '@editu/react';

function ThemeToggle() {
  const { theme, resolvedTheme, setTheme } = useEdituTheme();

  return (
    <button onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}>
      {resolvedTheme === 'dark' ? 'Light Mode' : 'Dark Mode'}
    </button>
  );
}

function App() {
  return (
    <EdituThemeProvider defaultTheme="system">
      <Editor />
      <ThemeToggle />
    </EdituThemeProvider>
  );
}

Components

EdituEditor

This component renders the editor content area.

tsx
<EdituEditor 
  editor={editor} 
  className="my-editor"
/>

Props

PropTypeDescription
editorEditor | nullEditor instance
classNamestringCustom class name

EdituBubbleMenu

This component displays a floating bubble menu on text selection.

tsx
<EdituBubbleMenu 
  editor={editor}
  className="my-bubble-menu"
  showDefaultMenu={true}
  updateDelay={100}
/>

Props

PropTypeDefaultDescription
editorEditor | null-Editor instance
classNamestring-Custom class name
showDefaultMenubooleantrueShow default bubble menu
pluginKeystring"edituBubbleMenu"Plugin key
updateDelaynumber100Position update delay
shouldShowFunction-Custom visibility logic
enableEmbedboolean-Enable embed in link editor

EdituThemeProvider

This component provides theme context.

tsx
<EdituThemeProvider 
  defaultTheme="system"
  storageKey="my-theme"
  disableTransitionOnChange={false}
>
  {children}
</EdituThemeProvider>

Props

PropTypeDefaultDescription
defaultTheme"light" | "dark" | "system""system"Default theme
storageKeystring"editu-theme"Storage key
targetSelectorstring-Theme attribute target
disableTransitionOnChangebooleanfalseDisable transitions

EdituSaveIndicator

This component displays the save status.

tsx
<EdituSaveIndicator 
  status={status} 
  lastSaved={lastSaved}
  className="my-indicator"
/>

EdituPortal

This component renders children in a portal.

tsx
<EdituPortal container={document.body}>
  <div className="my-overlay">Content</div>
</EdituPortal>

EdituIcon

This component renders an icon from the icon context. It uses Iconify icon IDs by default, and can be customized via EdituIconProvider.

tsx
import { EdituIcon } from '@editu/react';

<EdituIcon name="bold" className="my-icon" />

Patterns

Working with Markdown

tsx
import { Editu } from '@editu/react';

// Simple: initialMarkdown prop
function SimpleMarkdownEditor() {
  return (
    <Editu 
      initialMarkdown="# Hello World\n\nStart editing..."
      onUpdate={({ editor }) => {
        const md = editor.getMarkdown();
        console.log(md);
      }}
    />
  );
}

// Advanced: Two-way sync with useEdituMarkdown
function TwoWayMarkdownSync() {
  const editor = useEdituEditor({
    initialMarkdown: '# Hello',
  });
  const { markdown, setMarkdown, isPending } = useEdituMarkdown(() => editor);

  return (
    <div className="split-view">
      <EdituEditor editor={editor} />
      <div>
        <textarea 
          value={markdown} 
          onChange={(e) => setMarkdown(e.target.value)} 
        />
        {isPending && <span>Syncing...</span>}
      </div>
    </div>
  );
}

Controlled Content (JSON)

tsx
function ControlledEditor() {
  const [content, setContent] = useState<JSONContent>({
    type: 'doc',
    content: [],
  });

  const editor = useEdituEditor({
    initialContent: content,
    onUpdate: ({ editor }) => {
      setContent(editor.getJSON());
    },
  });

  return <EdituEditor editor={editor} />;
}

With Form

tsx
function EditorForm() {
  const editor = useEdituEditor();

  const handleSubmit = (e: FormEvent) => {
    e.preventDefault();
    if (editor) {
      const content = editor.getJSON();
      // Submit content
    }
  };

  return (
    <form onSubmit={handleSubmit}>
      <EdituEditor editor={editor} />
      <button type="submit">Submit</button>
    </form>
  );
}

With Ref

tsx
function EditorWithRef() {
  const editorRef = useRef<Editor | null>(null);
  
  const editor = useEdituEditor({
    onCreate: ({ editor }) => {
      editorRef.current = editor;
    },
  });

  const focusEditor = () => {
    editorRef.current?.commands.focus();
  };

  return (
    <div>
      <button onClick={focusEditor}>Focus</button>
      <EdituEditor editor={editor} />
    </div>
  );
}

Custom Bubble Menu

tsx
function CustomBubbleMenu({ editor }: { editor: Editor | null }) {
  if (!editor) return null;

  return (
    <div className="bubble-menu">
      <button
        onClick={() => editor.chain().focus().toggleBold().run()}
        className={editor.isActive('bold') ? 'active' : ''}
      >
        Bold
      </button>
      <button
        onClick={() => editor.chain().focus().toggleItalic().run()}
        className={editor.isActive('italic') ? 'active' : ''}
      >
        Italic
      </button>
      <button onClick={() => editor.chain().focus().undo().run()}>
        Undo
      </button>
      <button onClick={() => editor.chain().focus().redo().run()}>
        Redo
      </button>
    </div>
  );
}

SSR Considerations

The editor runs on the client side only. Use dynamic import or check for the browser environment:

tsx
import dynamic from 'next/dynamic';

const Editor = dynamic(() => import('./Editor'), {
  ssr: false,
  loading: () => <div>Loading editor...</div>,
});

Or with a client boundary:

tsx
'use client';

import { EdituEditor, useEdituEditor } from '@editu/react';

export function Editor() {
  const editor = useEdituEditor();
  return <EdituEditor editor={editor} />;
}

Next Steps

Released under the MIT License.