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.