Using Tiptap with the React Composable API
Tiptap provides a declarative <Tiptap> component that simplifies editor setup and automatically provides context to all child components. This composable API is an alternative to the hook-based useEditor + <EditorContent /> approach, offering a more React-idiomatic way to work with Tiptap.
When to use the Composable API
The composable API is ideal when you:
- Want a more declarative, component-based approach
- Need to access the editor from multiple child components
- Prefer automatic context management over manual prop passing
- Are building complex editor UIs (toolbars, menus, sidebars, blocks)
For simpler use cases or when you need more direct control, the hook-based useEditor approach may be more appropriate.
Installation
Before using the composable API, make sure you have Tiptap installed in your React project. Follow the React installation guide to set up the required dependencies.
Using the Tiptap component
The <Tiptap> component is the root provider that makes the editor instance available to all child components via React context.
Basic setup
Create a new React component and import the Tiptap component along with useEditor from @tiptap/react (menu components are imported from @tiptap/react/menus):
// src/Editor.tsx
"use client"
import { Tiptap, useEditor } from '@tiptap/react'
import { BubbleMenu, FloatingMenu } from '@tiptap/react/menus'
import StarterKit from '@tiptap/starter-kit'
function Editor() {
const editor = useEditor({
extensions: [StarterKit],
content: '<p>Hello World!</p>',
})
if (!editor) return null
return (
<Tiptap editor={editor}>
<Tiptap.Content />
<BubbleMenu editor={editor}>
<button>Bold</button>
<button>Italic</button>
</BubbleMenu>
<FloatingMenu editor={editor}>
<button>Add heading</button>
</FloatingMenu>
</Tiptap>
)
}
export default EditorAvailable subcomponents
| Component | Description |
|---|---|
Tiptap.Content | Renders the editor content area. Replaces <EditorContent editor={editor} />. |
Menu components are imported separately from @tiptap/react/menus.
Accessing the editor in child components
One of the main benefits of the composable API is that child components can access the editor without prop drilling.
Using the useTiptap hook
The useTiptap hook returns the editor instance from context.
import { useTiptap } from '@tiptap/react'
function MenuBar() {
const { editor } = useTiptap()
if (!editor) return null
return (
<div className="menu-bar">
<button
onClick={() => editor.chain().focus().toggleBold().run()}
className={editor.isActive('bold') ? 'is-active' : ''}
>
Bold
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? 'is-active' : ''}
>
Italic
</button>
</div>
)
}Then include the menu bar anywhere inside your <Tiptap> component:
<Tiptap editor={editor}>
<MenuBar />
<Tiptap.Content />
</Tiptap>Using useTiptapState for reactive state
For performance-sensitive components, use useTiptapState to subscribe to specific parts of the editor state. This prevents unnecessary re-renders when unrelated state changes.
import { useTiptapState } from '@tiptap/react'
function WordCount() {
const { editor } = useTiptap()
const wordCount = useTiptapState((state) => {
const text = state.editor.state.doc.textContent
return text.split(/\s+/).filter(Boolean).length
})
if (!editor) {
return null
}
return <span>{wordCount} words</span>
}The selector function receives an EditorStateSnapshot and should return only the data your component needs. The component will only re-render when the selected value changes.
Server-side rendering (SSR)
The composable API works seamlessly with server-side rendering. Since the editor instance is only created on the client, you can guard rendering until it exists:
"use client"
import { Tiptap, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
export function MyEditor() {
const editor = useEditor({
extensions: [StarterKit],
content: '<p>Hello World!</p>',
})
if (!editor) {
return <div className="skeleton">Loading editor...</div>
}
return (
<Tiptap editor={editor}>
<Tiptap.Content />
</Tiptap>
)
}Performance considerations
The composable API is designed with performance in mind:
- Automatic context optimization: The editor context is memoized to prevent unnecessary re-renders
- Selective subscriptions: Use
useTiptapStateto subscribe only to the state you need
For more performance tips, see the React Performance Guide.
Backwards compatibility
The <Tiptap> component automatically provides the EditorContext, which means you can use the useCurrentEditor hook inside it for backwards compatibility with existing code:
import { useCurrentEditor } from '@tiptap/react'
function EditorJSONPreview() {
const { editor } = useCurrentEditor()
if (!editor) return null
return <pre>{JSON.stringify(editor.getJSON(), null, 2)}</pre>
}However, for new code, we recommend using useTiptap().
API Reference
Tiptap component
The root provider component that makes the editor instance available via React context.
Props:
| Prop | Type | Description |
|---|---|---|
editor | Editor | null | The editor instance from useEditor() |
children | ReactNode | Child components |
Example:
<Tiptap editor={editor}>
<Tiptap.Content />
</Tiptap>useTiptap hook
Returns the Tiptap context value.
Returns:
| Property | Type | Description |
|---|---|---|
editor | Editor | null | The editor instance |
Example:
const { editor } = useTiptap()
if (!editor) return nulluseTiptapState hook
Subscribes to a slice of the editor state using a selector function.
Signature:
const value = useTiptapState(selector, equalityFn?)Parameters:
| Parameter | Type | Description |
|---|---|---|
selector | (state: EditorStateSnapshot) => T | Function to select state |
equalityFn | (a: T, b: T) => boolean | Optional equality function to control re-renders |
Example:
const isBold = useTiptapState((state) =>
state.editor.isActive('bold')
)Comparison: Composable API vs Hook-based Approach
| Feature | Composable API | Hook-based Approach |
|---|---|---|
| Setup complexity | Low – declarative components | Medium – manual prop passing |
| Context management | Automatic | Manual via EditorContext.Provider |
| Child component access | Easy via useTiptap() | Requires prop drilling or context |
| SSR support | Guarded render (if (!editor)) | Manual null checks |
| Performance | Optimized with useTiptapState | Optimized with useEditorState |
| Best for | Complex UIs with many child components | Simple UIs or direct control needed |