React 绑定
自 v4 起,Hocuspocus 通过 @hocuspocus/provider-react 包提供了专门的 React 绑定。它提供了两个用于管理 WebSocket 连接和房间生命周期的组件,以及用于订阅 provider 状态(连接、同步、awareness、事件)的 hooks——全部基于 useSyncExternalStore 构建,并且对 React StrictMode 安全。
安装
npm install @hocuspocus/provider @hocuspocus/provider-react yjsPeer dependencies:React 18 或 19,Yjs ^13.6.8,以及与之匹配版本的 @hocuspocus/provider。
快速开始
使用 HocuspocusProviderWebsocketComponent(它管理共享的 WebSocket)和一个或多个 HocuspocusRoom 组件(每个文档一个)包裹你的协作子树。在房间内部,像 useHocuspocusProvider、useHocuspocusAwareness 和 useHocuspocusConnectionStatus 这样的 hooks 可以让你访问 provider。
import {
HocuspocusProviderWebsocketComponent,
HocuspocusRoom,
} from '@hocuspocus/provider-react'
export function App() {
return (
<HocuspocusProviderWebsocketComponent url="ws://127.0.0.1:1234">
<HocuspocusRoom name="example-document">
<Editor />
</HocuspocusRoom>
</HocuspocusProviderWebsocketComponent>
)
}websocket 组件会为其子树创建一个单独的 HocuspocusProviderWebsocket。多个 HocuspocusRoom 可以共享它,这样在文档之间切换时就能避免连接开销(也就是多路复用)。
组件
HocuspocusProviderWebsocketComponent
管理共享的 HocuspocusProviderWebsocket 实例。在应用顶部附近创建一次即可。
<HocuspocusProviderWebsocketComponent url="ws://127.0.0.1:1234">
{children}
</HocuspocusProviderWebsocketComponent>Props
| Prop | Type | Description |
|---|---|---|
url | string | Hocuspocus 服务器 URL。除非提供了 websocketProvider,否则为必需项。 |
websocketProvider | HocuspocusProviderWebsocket | 提供你自己的 socket 实例以获得完全控制。与 url 互斥。 |
children | ReactNode | 通常是一个或多个 HocuspocusRoom。 |
该组件会优雅地处理 React StrictMode 的双重挂载——WebSocket 每个挂载周期只创建一次,并且只有在不是外部提供时才会销毁。
HocuspocusRoom
创建一个文档专属的 HocuspocusProvider,并将其连接到共享的 WebSocket。必须渲染在 HocuspocusProviderWebsocketComponent 内部。
<HocuspocusRoom
name="example-document"
token={async () => fetchJwt()}
onAuthenticationFailed={(data) => console.error(data.reason)}
onSynced={() => console.log('已同步')}
>
<Editor />
</HocuspocusRoom>Props
| Prop | Type | Description |
|---|---|---|
name | string | 文档名称。必需。 |
document | Y.Doc | 可选——传入你自己的 Y.Doc。如果省略,将为你创建一个。 |
token | string | (() => string) | (() => Promise<string>) | 发送到服务器用于身份验证的 JWT(或异步解析函数)。 |
onOpen, onConnect, onClose, onDisconnect, onStatus, onSynced, onUnsyncedChanges, onMessage, onOutgoingMessage, onStateless, onAuthenticated, onAuthenticationFailed, onAwarenessUpdate, onAwarenessChange, onDestroy | Function | 每个 provider 事件 的可选处理器。 |
更改 name、document、token 或上游 websocket provider 会重新创建底层 provider。其余内容都会保持稳定。
处理身份验证失败
当服务器的 onAuthenticate 钩子抛出异常时,provider 会发出 authenticationFailed 事件,并调用 HocuspocusRoom 上的 onAuthenticationFailed 处理器,参数为:
{ reason: string } // 服务器抛出的错误消息典型的用户体验:清除过期 token,显示登录界面,然后用新的 token 重新渲染。
<HocuspocusRoom
name="example-document"
token={token}
onAuthenticationFailed={({ reason }) => {
console.warn('认证失败:', reason)
setToken(null) // 进入你的登录流程
}}
>
<Editor />
</HocuspocusRoom>如果你更喜欢在嵌套组件中响应它,同样可以通过 useHocuspocusEvent('authenticationFailed', handler) 使用该事件。
Hooks
下面所有 hooks 都必须在 HocuspocusRoom 内部使用。如果没有 room 上下文,它们会抛出错误。
useHocuspocusProvider
返回当前房间的 HocuspocusProvider 实例。当你需要直接访问 provider 时使用它——例如将 Tiptap 的 Collaboration / CollaborationCaret 扩展连接到 provider 的 Y.Doc 和 awareness。
import { useHocuspocusProvider } from '@hocuspocus/provider-react'
import { useEditor, EditorContent } from '@tiptap/react'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
import { StarterKit } from '@tiptap/starter-kit'
function Editor() {
const provider = useHocuspocusProvider()
const editor = useEditor({
extensions: [
StarterKit.configure({ undoRedo: false }),
Collaboration.configure({ document: provider.document }),
CollaborationCaret.configure({
provider,
user: { name: 'John Doe', color: '#ffcc00' },
}),
],
})
return <EditorContent editor={editor} />
}useHocuspocusConnectionStatus
订阅 WebSocket 连接状态。返回 'connecting' | 'connected' | 'disconnected'。
import { useHocuspocusConnectionStatus } from '@hocuspocus/provider-react'
function ConnectionIndicator() {
const status = useHocuspocusConnectionStatus()
return (
<div className={`status-${status}`}>
{status === 'connected'
? '在线'
: status === 'connecting'
? '正在连接…'
: '离线'}
</div>
)
}useHocuspocusSyncStatus
订阅本地更改是否已与服务器同步。返回 'synced' | 'syncing'。
import { useHocuspocusSyncStatus } from '@hocuspocus/provider-react'
function SaveIndicator() {
const syncStatus = useHocuspocusSyncStatus()
return <div>{syncStatus === 'syncing' ? '正在保存…' : '所有更改已保存'}</div>
}useHocuspocusAwareness
订阅已连接用户列表(awareness 状态)。返回一个对象数组,每个对象包含 clientId,以及你在 awareness 上设置的任何状态(name、color、cursor 等)。
import { useHocuspocusAwareness } from '@hocuspocus/provider-react'
function UserList() {
const users = useHocuspocusAwareness()
return (
<div className="avatars">
{users.map((user) => (
<div
key={user.clientId}
style={{ backgroundColor: user.color as string }}
title={user.name as string}
>
{(user.name as string | undefined)?.[0]}
</div>
))}
</div>
)
}useHocuspocusEvent
通过稳定订阅监听任意 provider 事件(处理器引用会在内部保持最新,因此你不需要做 memoize)。
import { useHocuspocusEvent } from '@hocuspocus/provider-react'
function AuthGuard() {
useHocuspocusEvent('authenticationFailed', (data) => {
console.error('认证失败:', data.reason)
redirectToLogin()
})
useHocuspocusEvent('close', (data) => {
console.log('连接已关闭', data.event.code, data.event.reason)
})
return null
}完整示例 — Tiptap + awareness
将这些全部组合起来:一个 Tiptap 编辑器、一个连接指示器和一个用户列表:
import {
HocuspocusProviderWebsocketComponent,
HocuspocusRoom,
useHocuspocusAwareness,
useHocuspocusConnectionStatus,
useHocuspocusProvider,
} from '@hocuspocus/provider-react'
import { EditorContent, useEditor } from '@tiptap/react'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
import { StarterKit } from '@tiptap/starter-kit'
function Editor() {
const provider = useHocuspocusProvider()
const status = useHocuspocusConnectionStatus()
const users = useHocuspocusAwareness()
const editor = useEditor({
extensions: [
StarterKit.configure({ undoRedo: false }),
Collaboration.configure({ document: provider.document }),
CollaborationCaret.configure({
provider,
user: { name: 'John Doe', color: '#ffcc00' },
}),
],
})
return (
<>
<header>
<span>状态:{status}</span>
<span>{users.length} 在线</span>
</header>
<EditorContent editor={editor} />
</>
)
}
export function App() {
return (
<HocuspocusProviderWebsocketComponent url="ws://127.0.0.1:1234">
<HocuspocusRoom name="example-document">
<Editor />
</HocuspocusRoom>
</HocuspocusProviderWebsocketComponent>
)
}切换文档(多路复用)
由于 WebSocket 位于外层组件,切换 HocuspocusRoom 上的 name 属性会销毁旧的文档提供器,并在不重新连接套接字的情况下创建一个新的:
function Workspace({ docId }: { docId: string }) {
return (
<HocuspocusProviderWebsocketComponent url="ws://127.0.0.1:1234">
<HocuspocusRoom name={docId}>
<Editor />
</HocuspocusRoom>
</HocuspocusProviderWebsocketComponent>
)
}如果你希望在同一个套接字上同时打开多个文档,请将多个 HocuspocusRoom 作为同级元素渲染。当连接到 v4 服务器并对许多可能存在文档名冲突的提供器进行多路复用时,可以考虑在底层的 HocuspocusProviderWebsocket 上启用 sessionAwareness。