React
React 19 components and hooks for Editu editor.
Installation
npm install @editu/react
# or
pnpm add @editu/react
# or
yarn add @editu/reactRequirements
- React 19
- React DOM 19
Quick Start
Use the Editu component:
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:
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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
initialContent | JSONContent | - | Initial content (JSON) |
initialMarkdown | string | - | Initial content (Markdown) |
placeholder | string | - | Placeholder text |
editable | boolean | true | Editable state |
autofocus | boolean | 'start' | 'end' | 'all' | number | - | Auto focus |
features | EdituFeatureOptions | - | Feature options |
className | string | - | CSS class |
showToolbar | boolean | false | Show fixed toolbar above editor |
showBubbleMenu | boolean | true | Show bubble menu |
enableEmbed | boolean | - | Enable embed in links |
extensions | Extensions | - | Additional Tiptap extensions |
transformDiagramsOnImport | boolean | true | Transform diagram code blocks on import |
onUpdate | Function | - | Update callback |
onCreate | Function | - | Create callback |
onDestroy | Function | - | Destroy callback |
onSelectionUpdate | Function | - | Selection change callback |
onFocus | Function | - | Focus callback |
onBlur | Function | - | Blur callback |
Hooks
useEdituEditor
This hook creates and manages a Editu editor instance.
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.
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.
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:
| Property | Type | Description |
|---|---|---|
isFocused | boolean | Whether the editor is focused |
isEmpty | boolean | Whether the editor is empty |
canUndo | boolean | Whether undo is available |
canRedo | boolean | Whether redo is available |
characterCount | number | Character count |
wordCount | number | Word count |
useEdituAutoSave
This hook automatically saves editor content.
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.
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
| Property | Type | Description |
|---|---|---|
markdown | string | Current Markdown content |
setMarkdown | (md: string) => void | Update editor from Markdown |
isPending | boolean | Whether sync is pending |
useEdituTheme
This hook accesses theme state within EdituThemeProvider.
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.
<EdituEditor
editor={editor}
className="my-editor"
/>Props
| Prop | Type | Description |
|---|---|---|
editor | Editor | null | Editor instance |
className | string | Custom class name |
EdituBubbleMenu
This component displays a floating bubble menu on text selection.
<EdituBubbleMenu
editor={editor}
className="my-bubble-menu"
showDefaultMenu={true}
updateDelay={100}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
editor | Editor | null | - | Editor instance |
className | string | - | Custom class name |
showDefaultMenu | boolean | true | Show default bubble menu |
pluginKey | string | "edituBubbleMenu" | Plugin key |
updateDelay | number | 100 | Position update delay |
shouldShow | Function | - | Custom visibility logic |
enableEmbed | boolean | - | Enable embed in link editor |
EdituThemeProvider
This component provides theme context.
<EdituThemeProvider
defaultTheme="system"
storageKey="my-theme"
disableTransitionOnChange={false}
>
{children}
</EdituThemeProvider>Props
| Prop | Type | Default | Description |
|---|---|---|---|
defaultTheme | "light" | "dark" | "system" | "system" | Default theme |
storageKey | string | "editu-theme" | Storage key |
targetSelector | string | - | Theme attribute target |
disableTransitionOnChange | boolean | false | Disable transitions |
EdituSaveIndicator
This component displays the save status.
<EdituSaveIndicator
status={status}
lastSaved={lastSaved}
className="my-indicator"
/>EdituPortal
This component renders children in a portal.
<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.
import { EdituIcon } from '@editu/react';
<EdituIcon name="bold" className="my-icon" />Patterns
Working with Markdown
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)
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
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
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
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:
import dynamic from 'next/dynamic';
const Editor = dynamic(() => import('./Editor'), {
ssr: false,
loading: () => <div>Loading editor...</div>,
});Or with a client boundary:
'use client';
import { EdituEditor, useEdituEditor } from '@editu/react';
export function Editor() {
const editor = useEdituEditor();
return <EdituEditor editor={editor} />;
}Next Steps
- Configuration - Editor options
- Features - Enable and configure features
- Theming - Customize appearance
- API Reference