在 React 组合式 API 中使用 Tiptap

Editor

Tiptap 提供了一个声明式的 <Tiptap> 组件,简化了编辑器的设置,并自动为所有子组件提供上下文。该组合式 API 是基于钩子的 useEditor + <EditorContent /> 方式的替代方案,提供了更符合 React 习惯的 Tiptap 使用方式。

何时使用组合式 API

当你满足以下需求时,组合式 API 是理想选择:

  • 想要更声明式、基于组件的方法
  • 需要在多个子组件中访问编辑器实例
  • 希望自动管理上下文而非手动传递 props
  • 想使用内置的加载状态和支持服务端渲染 (SSR) 的模式

对于更简单的用例或需要更直接控制时,基于钩子的 useEditor 方式 可能更合适。

安装

在使用组合式 API 之前,请确保在你的 React 项目中已安装 Tiptap。请按照 React 安装指南 设置所需依赖。

使用 Tiptap 组件

<Tiptap> 组件是根提供者,通过 React 上下文将编辑器实例提供给所有子组件。

基本设置

创建一个新组件,并导入 Tiptap 组件和 useEditor

// src/Editor.tsx
import { Tiptap, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function Editor() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World!</p>',
  })

  return (
    <Tiptap instance={editor}>
      <Tiptap.Loading>加载编辑器中...</Tiptap.Loading>
      <MenuBar />
      <Tiptap.Content />
      <Tiptap.BubbleMenu>
        <button>加粗</button>
        <button>斜体</button>
      </Tiptap.BubbleMenu>
      <Tiptap.FloatingMenu>
        <button>添加标题</button>
      </Tiptap.FloatingMenu>
    </Tiptap>
  )
}

export default Editor

可用子组件

<Tiptap> 组件包含多个处理常见编辑器 UI 模式的子组件:

组件说明
Tiptap.Content渲染编辑器内容区域。替代 <EditorContent editor={editor} />
Tiptap.Loading仅在编辑器初始化时渲染其子内容。适合加载状态显示。
Tiptap.BubbleMenu基于文本选择上下文的气泡菜单。
Tiptap.FloatingMenu基于空白行上下文的悬浮菜单。

在子组件中访问编辑器

组合式 API 的主要优势之一是子组件可以无需传递 props 直接访问编辑器实例。

使用 useTiptap 钩子

useTiptap 钩子返回编辑器实例和一个表示编辑器初始化完成的 isReady 标志。

import { useTiptap } from '@tiptap/react'

function MenuBar() {
  const { editor, isReady } = useTiptap()

  if (!isReady || !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 instance={editor}>
  <MenuBar />
  <Tiptap.Content />
</Tiptap>

使用 useTiptapState 订阅响应式状态

针对性能敏感的组件,可使用 useTiptapState 订阅编辑器状态的特定部分。这样能避免无关状态变化导致的不必要重新渲染。

import { useTiptap, useTiptapState } from '@tiptap/react'

function WordCount() {
  const { isReady } = useTiptap()

  const wordCount = useTiptapState((state) => {
    const text = state.editor.state.doc.textContent
    return text.split(/\s+/).filter(Boolean).length
  })

  if (!isReady) {
    return null
  }

  return <span>{wordCount} 字</span>
}

选择器函数接收一个 EditorStateSnapshot,应返回组件需要的数据。只有当选中值发生变化时,组件才会重新渲染。

重要

仅在编辑器准备好后使用 useTiptapState。请先通过 useTiptap() 检查 isReady,再渲染使用该钩子的组件。

服务端渲染(SSR)

组合式 API 与服务端渲染无缝配合。利用 immediatelyRender 选项防止编辑器在服务器端渲染,同时用 Tiptap.Loading 显示占位内容:

'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>',
    immediatelyRender: false,
  })

  return (
    <Tiptap instance={editor}>
      <Tiptap.Loading>
        <div className="skeleton">加载编辑器中...</div>
      </Tiptap.Loading>
      <Tiptap.Content />
    </Tiptap>
  )
}

Tiptap.Loading 组件在 SSR 场景下十分有用,可以在客户端初始化编辑器之前显示占位符。

性能考虑

组合式 API 设计时注重性能:

  • 自动上下文优化:编辑器上下文经过 memo 优化,避免不必要重新渲染
  • 选择性订阅:用 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>
}

不过,建议新代码使用提供额外上下文(如 isReady 标志)的 useTiptap()

API 参考

Tiptap 组件

根提供者组件,通过 React 上下文提供编辑器实例。

Props:

属性类型说明
instanceEditor | nulluseEditor() 返回的编辑器实例
childrenReactNode子组件

示例:

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

useTiptap 钩子

返回 Tiptap 上下文值。

返回值:

属性类型说明
editorEditor | null编辑器实例
isReadyboolean编辑器初始化完成时为 true

示例:

const { editor, isReady } = useTiptap()

if (!isReady || !editor) {
  return null
}

useTiptapState 钩子

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

签名:

const value = useTiptapState(selector, equalityFn?)

参数:

参数类型说明
selector(state: EditorStateSnapshot) => T选择状态的函数
equalityFn(a: T, b: T) => boolean可选,自定义判断相等函数。默认为 fast-equals 提供的深度比较函数。

示例:

const isBold = useTiptapState((state) => state.editor.isActive('bold'))

对比:组合式 API 与基于钩子的方式

特性组合式 API基于钩子的方式
设置复杂度低 - 声明式组件中 - 需手动传递 props
上下文管理自动手动通过 EditorContext.Provider
子组件访问通过 useTiptap() 轻松访问需传递 props 或使用上下文
加载状态内建 Tiptap.Loading需手动实现
SSR 支持通过 Tiptap.Loading 内建支持需手动空值判断
性能使用 useTiptapState 优化使用 useEditorState 优化
适用场景包含许多子组件的复杂 UI简单 UI 或需要直接控制

下一步