在 React 组合式 API 中使用 Tiptap

Editor

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:

属性类型说明
editorEditor | null来自 useEditor() 的编辑器实例
childrenReactNode子组件

示例:

<Tiptap editor={editor}>
  <Tiptap.Content />
</Tiptap>

useTiptap 钩子

返回 Tiptap 上下文值。

返回值:

属性类型说明
editorEditor | null编辑器实例

示例:

const { editor } = useTiptap()

if (!editor) return null

useTiptapState 钩子

通过选择器函数订阅编辑器状态的部分片段。

签名:

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 或需要直接控制时

下一步