React 集成与性能优化
1. 核心思想:把 Editor 实例生命周期从组件渲染中剥离
@tiptap/react 的关键不是“渲染一个编辑器”,而是“稳定管理一个长生命周期 Editor 实例,再按需订阅状态变化”。
对应实现:
useEditor.ts:EditorInstanceManageruseEditorState.ts:EditorStateManagerEditorContent.tsx:DOM 宿主与 NodeView Portal 桥接
2. useEditor 的实例管理
2.1 为什么不用简单 useState(new Editor())
直接在组件里新建实例,会在依赖变化、StrictMode 双调用、卸载重挂时产生重复创建/销毁问题。
Tiptap 采用 EditorInstanceManager 来做:
- options 比较更新(尽量
setOptions而不是重建) - deps 变化时才重建实例
scheduleDestroy延迟销毁,防止同一 tick 的“误杀”
2.2 useSyncExternalStore 的价值
useEditor 和 useEditorState 都使用 useSyncExternalStore 系列 API:
- 与 React 并发模型兼容
- 将“外部可变对象(Editor)”纳入可预测订阅模式
- 降低无关 re-render
3. SSR 与 immediatelyRender
useEditor 内部有 SSR/Next 场景判断:
- SSR 环境推荐
immediatelyRender: false,避免 hydration mismatch。 - CSR 可设为
true获得更直接实例语义。
证据:
可说:这不是“功能开关”,本质是客户端 DOM 可用性与 hydration 一致性约束。
源码片段(packages/react/src/useEditor.ts):
ts
if (this.options.current.immediatelyRender === undefined) {
if (isSSR || isNext) {
throw new Error(
'Tiptap Error: SSR has been detected, please set `immediatelyRender` explicitly to `false` ...',
)
}
return this.createEditor()
}4. 事务驱动渲染控制
默认情况下,历史行为会在每次 transaction 触发重渲染;v3 引入更细粒度控制:
shouldRerenderOnTransaction(兼容开关)useEditorState(selector, equalityFn)(推荐)
这样可以让工具栏只订阅 isBold/isItalic 等所需状态,而不是全量跟着文档变动重渲染。
5. EditorContent 与 Portal NodeViews
EditorContent.tsx 做了两件事:
- 接管并迁移 ProseMirror 生成的 DOM 到 React 容器。
- 通过
contentComponent维护 NodeView ReactRenderer 的 portal 映射。
这让 NodeView 的 React 组件仍可使用 React 生命周期,同时保持 ProseMirror 对 contentDOM 的控制。
6. BubbleMenu 回归案例:响应性 vs 稳定性
相关:
经验点:
- 把更多 props 放进 effect 依赖可提高“响应变化能力”。
- 但可能导致插件反复卸载/重建,引发 selection 与事务抖动。
- 最终要做“可更新字段”和“实例级配置字段”的边界划分。
这是前端框架桥接层非常典型的设计权衡题。
7. 延伸追问建议
如果被问“你会怎么做 React 优化”:
useEditor实例尽量稳定,避免每次 render new Editor。- 使用
useEditorState+ selector 控制订阅粒度。 - 避免把未 memo 的复杂对象直接传给菜单类组件。
- SSR 场景明确设置
immediatelyRender,不要依赖隐式判断。