React bindings
Since v4, Hocuspocus ships with dedicated React bindings via the @hocuspocus/provider-react package. It exposes two components for managing the WebSocket connection and room lifecycle, plus hooks for subscribing to provider state (connection, sync, awareness, events) — all built on useSyncExternalStore and React StrictMode–safe.
Install
npm install @hocuspocus/provider @hocuspocus/provider-react yjsPeer dependencies: React 18 or 19, Yjs ^13.6.8, and a matching version of @hocuspocus/provider.
Getting started
Wrap your collaborative subtree with HocuspocusProviderWebsocketComponent (which manages the shared WebSocket) and one or more HocuspocusRoom components (one per document). Inside the room, hooks like useHocuspocusProvider, useHocuspocusAwareness, and useHocuspocusConnectionStatus give you access to the 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>
)
}The websocket component creates a single HocuspocusProviderWebsocket for its subtree. Multiple HocuspocusRooms can share it, which avoids connection overhead when switching between documents (aka multiplexing).
Components
HocuspocusProviderWebsocketComponent
Manages the shared HocuspocusProviderWebsocket instance. Create it once near the top of your app.
<HocuspocusProviderWebsocketComponent url="ws://127.0.0.1:1234">
{children}
</HocuspocusProviderWebsocketComponent>Props
| Prop | Type | Description |
|---|---|---|
url | string | The Hocuspocus server URL. Required unless websocketProvider is provided. |
websocketProvider | HocuspocusProviderWebsocket | Bring your own socket instance for full control. Mutually exclusive with url. |
children | ReactNode | Usually one or more HocuspocusRooms. |
The component handles React StrictMode double-mounts gracefully — the WebSocket is created once per mount cycle and only destroyed if it wasn't externally provided.
HocuspocusRoom
Creates a document-specific HocuspocusProvider that attaches to the shared WebSocket. Must be rendered inside HocuspocusProviderWebsocketComponent.
<HocuspocusRoom
name="example-document"
token={async () => fetchJwt()}
onAuthenticationFailed={(data) => console.error(data.reason)}
onSynced={() => console.log('synced')}
>
<Editor />
</HocuspocusRoom>Props
| Prop | Type | Description |
|---|---|---|
name | string | Document name. Required. |
document | Y.Doc | Optional — bring your own Y.Doc. If omitted, one is created for you. |
token | string | (() => string) | (() => Promise<string>) | JWT (or async resolver) sent to the server for authentication. |
onOpen, onConnect, onClose, onDisconnect, onStatus, onSynced, onUnsyncedChanges, onMessage, onOutgoingMessage, onStateless, onAuthenticated, onAuthenticationFailed, onAwarenessUpdate, onAwarenessChange, onDestroy | Function | Optional handlers for each provider event. |
Changing name, document, token, or the upstream websocket provider recreates the underlying provider. Everything else is kept stable.
Handling authentication failures
When the server's onAuthenticate hook throws, the provider emits an authenticationFailed event and the onAuthenticationFailed handler on HocuspocusRoom is called with:
{ reason: string } // the error message thrown on the serverTypical UX: clear the stale token, show a sign-in screen, then re-render with a fresh token.
<HocuspocusRoom
name="example-document"
token={token}
onAuthenticationFailed={({ reason }) => {
console.warn('Auth failed:', reason)
setToken(null) // drop into your sign-in flow
}}
>
<Editor />
</HocuspocusRoom>The same event is available via useHocuspocusEvent('authenticationFailed', handler) if you prefer to react to it from a nested component.
Hooks
All hooks below must be used inside a HocuspocusRoom. They throw if there is no room context.
useHocuspocusProvider
Returns the HocuspocusProvider instance for the current room. Use it when you need direct access to the provider — for example to wire Tiptap's Collaboration / CollaborationCaret extensions to the provider's Y.Doc and 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
Subscribe to the WebSocket connection status. Returns 'connecting' | 'connected' | 'disconnected'.
import { useHocuspocusConnectionStatus } from '@hocuspocus/provider-react'
function ConnectionIndicator() {
const status = useHocuspocusConnectionStatus()
return (
<div className={`status-${status}`}>
{status === 'connected'
? 'Online'
: status === 'connecting'
? 'Connecting…'
: 'Offline'}
</div>
)
}useHocuspocusSyncStatus
Subscribe to whether local changes have been synced with the server. Returns 'synced' | 'syncing'.
import { useHocuspocusSyncStatus } from '@hocuspocus/provider-react'
function SaveIndicator() {
const syncStatus = useHocuspocusSyncStatus()
return <div>{syncStatus === 'syncing' ? 'Saving…' : 'All changes saved'}</div>
}useHocuspocusAwareness
Subscribe to the list of connected users (awareness states). Returns an array of objects with a clientId plus whatever state you've set on 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
Subscribe to any provider event with a stable subscription (the handler ref is kept up to date internally, so you don't need to memoize).
import { useHocuspocusEvent } from '@hocuspocus/provider-react'
function AuthGuard() {
useHocuspocusEvent('authenticationFailed', (data) => {
console.error('Auth failed:', data.reason)
redirectToLogin()
})
useHocuspocusEvent('close', (data) => {
console.log('Connection closed', data.event.code, data.event.reason)
})
return null
}Full example — Tiptap + awareness
Putting it all together with a Tiptap editor, a connection indicator, and a user list:
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: {status}</span>
<span>{users.length} online</span>
</header>
<EditorContent editor={editor} />
</>
)
}
export function App() {
return (
<HocuspocusProviderWebsocketComponent url="ws://127.0.0.1:1234">
<HocuspocusRoom name="example-document">
<Editor />
</HocuspocusRoom>
</HocuspocusProviderWebsocketComponent>
)
}Switching documents (multiplexing)
Because the WebSocket lives on the outer component, swapping the name prop on HocuspocusRoom destroys the old document provider and creates a new one without reconnecting the socket:
function Workspace({ docId }: { docId: string }) {
return (
<HocuspocusProviderWebsocketComponent url="ws://127.0.0.1:1234">
<HocuspocusRoom name={docId}>
<Editor />
</HocuspocusRoom>
</HocuspocusProviderWebsocketComponent>
)
}If you want several documents open concurrently over the same socket, render multiple HocuspocusRooms as siblings. When connecting to a v4 server and multiplexing many providers with potentially colliding document names, consider enabling sessionAwareness on the underlying HocuspocusProviderWebsocket.