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 yjs

Peer 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

PropTypeDescription
urlstringThe Hocuspocus server URL. Required unless websocketProvider is provided.
websocketProviderHocuspocusProviderWebsocketBring your own socket instance for full control. Mutually exclusive with url.
childrenReactNodeUsually 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

PropTypeDescription
namestringDocument name. Required.
documentY.DocOptional — bring your own Y.Doc. If omitted, one is created for you.
tokenstring | (() => 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, onDestroyFunctionOptional 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 server

Typical 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.