在 React 组合式 API 中使用 Tiptap
Tiptap 提供了一个声明式的 <Tiptap> 组件,简化了编辑器的设置,并自动为所有子组件提供上下文。该组合式 API 是基于钩子的 useEditor + <EditorContent /> 方式的替代方案,提供了更符合 React 习惯的 Tiptap 使用方式。
何时使用组合式 API
当你满足以下需求时,组合式 API 是理想选择:
- 需要一种更具声明式、基于组件的方式
- 需要从多个子组件中访问编辑器
- 希望自动进行上下文管理,而不是手动传递 props
- 正在构建复杂的编辑器 UI(工具栏、菜单、侧边栏、区块)
对于更简单的用例或需要更直接控制时,基于钩子的 useEditor 方式 可能更合适。
安装
在使用组合式 API 之前,请确保在你的 React 项目中已安装 Tiptap。请按照 React 安装指南 设置所需依赖。
使用 Tiptap 组件
<Tiptap> 组件是根提供者,通过 React 上下文将编辑器实例提供给所有子组件。
基本设置
创建一个新的 React 组件,并从 @tiptap/react 中导入 Tiptap 组件以及 useEditor(菜单组件从 @tiptap/react/menus 导入):
// 源码/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 Editor可用子组件
| 组件 | 描述 |
|---|---|
Tiptap.Content | 渲染编辑器内容区域。替代 <EditorContent editor={editor} />。 |
菜单组件需要从 @tiptap/react/menus 中单独导入。
在子组件中访问编辑器
组合式 API 的主要优势之一是子组件可以无需传递 props 直接访问编辑器实例。
使用 useTiptap 钩子
useTiptap 钩子会从上下文中返回编辑器实例。
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' : ''}
>
加粗
</button>
<button
onClick={() => editor.chain().focus().toggleItalic().run()}
className={editor.isActive('italic') ? 'is-active' : ''}
>
斜体
</button>
</div>
)
}然后在你的 <Tiptap> 组件内的任意位置使用该菜单栏:
<Tiptap editor={editor}>
<MenuBar />
<Tiptap.Content />
</Tiptap>使用 useTiptapState 订阅响应式状态
针对性能敏感的组件,可使用 useTiptapState 订阅编辑器状态的特定部分。这样能避免无关状态变化导致的不必要重新渲染。
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} 字</span>
}选择器函数接收一个 EditorStateSnapshot,应返回组件需要的数据。只有当选中值发生变化时,组件才会重新渲染。
服务端渲染(SSR)
组合式 API 可与服务端渲染无缝协作。由于编辑器实例只会在客户端创建,因此你可以在它存在之前先阻止渲染:
"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>
)
}性能注意事项
组合式 API 在设计时注重性能:
- 自动上下文优化:编辑器上下文会被缓存,以防止不必要的重新渲染
- 选择性订阅:使用
useTiptapState只订阅你需要的状态
更多性能技巧见React 性能指南。
向后兼容性
<Tiptap> 组件自动提供了 EditorContext,因此可以在其内部使用 useCurrentEditor 钩子,以兼容现有代码:
import { useCurrentEditor } from '@tiptap/react'
function EditorJSONPreview() {
const { editor } = useCurrentEditor()
if (!editor) return null
return <pre>{JSON.stringify(editor.getJSON(), null, 2)}</pre>
}不过,对于新代码,我们建议使用 useTiptap()。
API 参考
Tiptap 组件
根提供者组件,通过 React 上下文提供编辑器实例。
Props:
| 属性 | 类型 | 说明 |
|---|---|---|
editor | Editor | null | 来自 useEditor() 的编辑器实例 |
children | ReactNode | 子组件 |
示例:
<Tiptap editor={editor}>
<Tiptap.Content />
</Tiptap>useTiptap 钩子
返回 Tiptap 上下文值。
返回值:
| 属性 | 类型 | 说明 |
|---|---|---|
editor | Editor | null | 编辑器实例 |
示例:
const { editor } = useTiptap()
if (!editor) return nulluseTiptapState 钩子
通过选择器函数订阅编辑器状态的部分片段。
签名:
const value = useTiptapState(selector, equalityFn?)参数:
| 参数 | 类型 | 说明 |
|---|---|---|
selector | (state: EditorStateSnapshot) => T | 用于选择状态的函数 |
equalityFn | (a: T, b: T) => boolean | 可选的相等性函数,用于控制重新渲染 |
示例:
const isBold = useTiptapState((state) =>
state.editor.isActive('bold')
)对比:组合式 API 与基于钩子的方式
| 特性 | 组合式 API | 基于钩子的方式 |
|---|---|---|
| 设置复杂度 | 低 – 声明式组件 | 中 – 手动传递 props |
| 上下文管理 | 自动 | 通过 EditorContext.Provider 手动管理 |
| 子组件访问 | 通过 useTiptap() 轻松访问 | 需要逐层传递 props 或使用 context |
| SSR 支持 | 受保护的渲染(if (!editor)) | 手动进行 null 检查 |
| 性能 | 使用 useTiptapState 优化 | 使用 useEditorState 优化 |
| 最适合 | 拥有许多子组件的复杂 UI | 简单 UI 或需要直接控制时 |