@stesura/collab-adapter-react
React bindings for collaboration: the mount hooks for the Pitter Patter
server tier and the local tier, rooms, presence and connection UI,
and the comment and proofread adapters that sync through the collab backend. The plugins underneath
are in @stesura/collab-adapter.
Server collaboration
useCollabEditor
Editor state synced through a Pitter Patter CollabAuthority. Reads docId, apiBasePath
and fetch from the nearest CollabProvider.
Returns the same shape as useLocalEditor (editorState, dispatch, pluginFactory,
schema) plus docStatus and reconnect. docStatus:
- “gone”: the doc was removed (410 from any loop). Terminal.
- “error”: hydration can never succeed (terminal status, failure ceiling, unparseable snapshot) or the server refuses this client’s protocol version (426). Terminal.
- “schema-mismatch”: the server refused this client’s schema at the handshake, the snapshot
names a node or mark the schema lacks, or a polled commit does not parse: the server or
another client runs a different schema. Terminal; a reload picks up the new bundle.
onErrorgets aSchemaMismatchErrorwith the unknown types or both fingerprints. - “disconnected”: a sync loop stopped on a terminal status or repeated failures. Recoverable:
the editor keeps the unconfirmed edits,
reconnect()resumes, and so does any send that gets through. Switching to view mode while disconnected is left to the host.
| Name | Type | Default |
|---|---|---|
__0 | UseCollabEditorArgs |
{ editorState: EditorState | null; dispatch: DispatchFn; pluginFactory: (args: { editorId: string; parentEditorId?: string; currentUser?: CurrentUser; }) => (Plugin<...> | Plugin<...>)[]; schema: Schema<...>; docStatus: "error" | ... 4 more ... | "disconnected"; reconnect: () => void | undefined; }UseCollabEditorArgs
Arguments for useCollabEditor.
The connection re-initialises only when the provider’s docId, apiBasePath or fetch
changes (or after a 412 resync). schema is fixed per mount, and editorId, configuration,
pluginOptions, extraPlugins, initialContent and presenceCursorDisplay are read when the
connection initialises; remount with a React key to change them. Read live:
transactionModifier, getPresenceDisplayName, getPresenceAvatar, onInlineImageRejected,
onUnmergedEdits, onError and the current user (presence re-announces when it resolves).
CollabProvider
Provides the collab document connection (docId, API base path, authenticated fetch) and
wires comments to the backend’s /threads routes; thread sync pauses while the page is hidden.
Pass comments={false} if the backend doesn’t serve the thread routes: the comments UI then
uses the no-op adapter instead of flagging thread sync as failed on the first 404.
headers and credentials apply to every request under this provider: sync, presence and
comments alike.
| Name | Type | Default |
|---|---|---|
__0 | CollabProviderProps |
string | number | bigint | boolean | Element | Iterable<ReactNode> | Promise<AwaitedReactNode> | nullCollabProviderProps
Props of CollabProvider.
UnmergedComment
Comments written offline are kept and sent once the server answers, across a tab close too. When
the server then refuses one, or its thread or document was deleted meanwhile, onUnmergedComment
gets it as UnmergedComment: the thread as the user left it, to tell them and offer their text
back.
A comment write made offline that was dropped when it was finally sent, handed to the host so
the user can be told and their text recovered. "refused": the server refused it (status);
"deleted": the thread was removed for good meanwhile; "gone": the document was.
| Name | Type | Default |
|---|---|---|
reason | "gone" | "refused" | "deleted" | |
threadId | string | |
thread | SerializedThreadThe thread as the user left it. | |
status | number |
useCollabDoc / useOptionalCollabDoc
The enclosing document’s docId (always set), apiBasePath and authenticated fetch.
Like useCollabDoc, but returns undefined outside a CollabProvider.
Both return a CollabDocContextType: the provider’s CollabConnection (docId, apiBasePath)
plus a fetch carrying its headers and credentials.
What useCollabDoc returns.
Local collaboration
The offline, multi-tab tier. The backend and its errors are in
@stesura/collab-adapter-local.
useLocalCollabEditor
Offline, multi-tab collaborative editor: the pitter-patter engine running against IndexedDB +
BroadcastChannel (@stesura/collab-adapter-local) instead of a server. Same shape as
useCollabEditor minus reconnect; no presence, threads or proofread cache.
Undo is plain prosemirror-history: the collab-commit plugin speaks prosemirror-collab’s
rebased/addToHistory protocol, so undo reverts only local edits.
docStatus:
- “deleted”: the doc row was removed (
deleteDoc); sync has stopped. Terminal. - “error”: the document cannot be loaded (terminal store error, failure ceiling, or a snapshot the schema cannot parse). Terminal.
- “schema-mismatch”: the snapshot names a node or mark this tab’s schema lacks, or another
tab’s commit does not parse: that tab runs a newer bundle. Terminal; a reload fixes it.
onErrorgets aSchemaMismatchErrornaming the unknown types. A tab that falls behind the retained commit window re-hydrates from the snapshot instead.
backend is the one syncing (the caller’s, else the shared default), e.g. for
createLocalHistorySource. Null during server rendering, where there is no IndexedDB.
| Name | Type | Default |
|---|---|---|
__0 | UseLocalCollabEditorArgs |
{ editorState: EditorState | null; dispatch: DispatchFn; pluginFactory: PluginCallback; schema: Schema<any, any>; docStatus: LocalCollabDocStatus; backend: LocalCollabBackend | null; }UseLocalCollabEditorArgs
Arguments for useLocalCollabEditor: useCollabEditor’s without the presence options and
initialContent, plus docId and an optional local backend.
LocalCollabDocStatus
export type LocalCollabDocStatus = "loading" | "ready" | "deleted" | "error" | "schema-mismatch";useLocalCollabEditor’s docStatus.
LocalCommentProvider
Comments for the local (IndexedDB) collab setup: the local counterpart of CollabProvider’s
thread wiring, over a LocalThreadStore. Use next to useLocalCollabEditor with the
same docId; threads sync across tabs.
| Name | Type | Default |
|---|---|---|
__0 | { docId: string; children: ReactNode; backend?: LocalCollabBackend; onError?: CollabErrorReporter; } |
Rooms
Multi-document suites. See Rooms & suites.
RoomProvider
Provides a room’s live manifest. Nest one CollabProvider per open document inside it; the
manifest lists what exists. Polling pauses while the page is hidden.
headers and credentials apply to the room requests only; pass the same ones to each nested
CollabProvider.
| Name | Type | Default |
|---|---|---|
__0 | RoomProviderProps |
RoomProviderProps
Props of RoomProvider.
useRoomManifest
The nearest room’s manifest plus mutations.
docs is never updated optimistically: addDoc/removeDoc resolve once the server has
written, and the change arrives through the same long-poll every client sees, so all clients
converge on one ordering.
failed means polling stopped for good; loading is false by then, so an empty room and one
that could not load are distinguishable.
{ docs: RoomDoc[]; loading: boolean; failed: boolean; addDoc: (entry: NewRoomDoc) => Promise<void>; removeDoc: (docId: string) => Promise<void>; }useRoom
The nearest room’s roomId and apiBasePath. Use useRoomManifest for its contents.
useRoomPresence
Who else is in the nearest room (one entry per client, so a user with two tabs shows twice;
group by userId for avatars), and setFocus to say what this client is on. Requires the
RoomProvider’s presence prop to announce this client.
{ clientId: string; others: RoomPresenceIndicator[]; setFocus: (focus: RoomPresenceFocus | null) => void; }RoomDoc
One document’s entry in a room manifest (matches the server’s RoomDocEntry).
| Name | Type | Default |
|---|---|---|
docId | string | |
title | string | |
position | number | |
schemaName | stringThe named schema the doc uses, for building its editor ( |
NewRoomDoc is what addDoc takes: a RoomDoc plus an optional initialContent.
Presence and connection
Both hooks read the sync loops the mount hook already runs, so they report nothing until that document’s editor is mounted. See Presence.
CollabPresenceRoster
Accessible list of who is in the document: the current user first, then each remote user (deduped by userId), with arrivals and departures announced through a polite live region.
The in-document carets are aria-hidden (ARIA inside .ProseMirror leaks into the prose), so
this is the only presence information screen readers get. Render it in the chrome, or build
your own on useDocPresence with the same two parts. Unstyled beyond a colour swatch per user.
| Name | Type | Default |
|---|---|---|
__0 | CollabPresenceRosterProps |
Props of CollabPresenceRoster.
useDocPresence
The latest raw presence snapshot for a document, keyed by clientId. Fed by useCollabEditor’s
presence poll, so it is empty until that document’s editor is mounted.
Unfiltered, unlike the presence plugin’s state: it includes the local user’s other sessions and
indicators with no document position. Dedupe by userId for a “who’s here” UI.
| Name | Type | Default |
|---|---|---|
docId | stringdefaults to the nearest |
DocPresenceSnapshot is what it returns: a Record<clientId, PresenceIndicator>.
useDocConnection
A document’s sync connection status. Fed by useCollabEditor’s own requests (no extra
network), so it stays “connecting” until that document’s editor is mounted.
| Name | Type | Default |
|---|---|---|
docId | stringdefaults to the nearest |
export type DocConnectionStatus = "connecting" | "online" | "offline";Health of a document’s server sync, written by useCollabEditor: “connecting” until its first
request resolves, then whether hydrate, commit polls and commit sends are getting through.
CollabConnectionStatus
A status-bar dot and label for the nearest CollabProvider’s document, from useDocConnection.
Renders nothing outside a provider. Mount it through a UI extension’s StatusBarElements.
Comments
CollabProvider and LocalCommentProvider wire these for you; use them directly to put comments
on a backend of your own. See the adapter contract.
createCollabCommentAdapter
A CommentAdapter over any ThreadMirror (ThreadStoreClient or LocalThreadStore):
whole-thread writes, soft deletes, reactions.
A new thread’s editor fragment comes from metadata.editorId (set by the comments UI),
falling back to "default"; later writes keep the stored record’s editorId.
| Name | Type | Default |
|---|---|---|
store | ThreadMirror | |
currentUser | { id: string; name?: string; avatar?: string; } |
Thread mirrors
ThreadMirror is the abstract in-memory mirror of a document’s threads that
createCollabCommentAdapter reads; each record is a ThreadMirrorRecord ({ editorId, data }).
Two implementations ship:
| Class | Syncs | Options |
|---|---|---|
ThreadStoreClient | Against the server’s /threads routes: hydrate, then long-poll. Writes PUT the whole thread (last write wins) | ThreadStoreClientOptions |
LocalThreadStore | In IndexedDB, across tabs over a BroadcastChannel | LocalThreadStoreOptions |
ThreadStoreClient stops with a ThreadSyncTerminatedError (its status says why) when the server
answers a status retrying can’t fix, such as an expired session.
Options for ThreadStoreClient.
Options for LocalThreadStore.
useThreadSyncFailed
Whether comment sync for docId has stopped for good (terminal status, too many failures in a
row, or a failed local IndexedDB load). The mirror keeps what it last held, so treat this as an
“out of date” banner rather than a reason to hide the panel.
| Name | Type | Default |
|---|---|---|
docId | string |
noopCommentAdapter
A CommentAdapter with no backend: no threads, and createThread returns an empty, unpersisted
thread. CollabProvider uses it when comments={false}.
Proofread
Proofread adapters stored through the collab backend. See Proofread.
createCollabProofreadCacheFactory
A ProofreadCacheAdapter factory backed by the /proofread/:field routes of
@stesura/collab-adapter-server (one shared bucket per doc + field). Pass it to
proofreadPlugin({ createCache }).
The adapter contract is synchronous, so each field keeps an in-memory mirror shared by all its
instances: one hydration GET when the first instance is created (never overriding locally
touched ids), writes debounced ~2s and POSTed with keepalive.
No live cross-tab or cross-client sync: a stale mirror only means a node gets re-checked.
| Name | Type | Default |
|---|---|---|
__0 | { docId: string; apiBasePath?: string; onError?: CollabErrorReporter; } & CollabTransportOptions |
createCollabProofreadDictionaryFactory
A ProofreadDictionaryAdapter factory backed by an app-level dictionary endpoint
({apiBasePath}/api/proofread/dictionary by default). Pass it to
proofreadPlugin({ createDictionary }).
Identity-scoped: the endpoint returns the session user’s effective dictionary and takes no doc
id, so document access never exposes a dictionary. Same sync model as the proofread cache
(in-memory mirror, one hydration, debounced keepalive write-through) plus cross-tab
BroadcastChannel updates; hydration is retried with backoff.
Known limitation: the retry queue is in memory. Writes still failing when the last adapter is destroyed are dropped, so a word added offline is lost if the page closes first.
| Name | Type | Default |
|---|---|---|
__0 | { apiBasePath?: string; endpoint?: string; onError?: CollabErrorReporter; } & CollabTransportOptions |
Guests
For demos and anonymous editing. Real apps pass their authenticated user to StesuraUserProvider.
useGuestUser
A guest CollabUser for demos and anonymous editing, stable for the component’s lifetime.
Name and colour derive from the id (guestProfile), so pass guestProfile to your
resolveUsers to show other guests the same way. Real apps should pass their authenticated
user to StesuraUserProvider.
| Name | Type | Default |
|---|---|---|
__0 | UseGuestUserOptions |
Options for useGuestUser.
guestProfile
A guest’s display profile, derived from the id alone: every peer computes the same name,
colour and avatar for the same id, so no user directory is needed. Names come from fixed word
lists, so a shared public document never shows anything a visitor typed. The colour is the one
the presence cursor already uses for this id. The avatar is picked from avatars; without
any, it is empty and the UI falls back to initials.
| Name | Type | Default |
|---|---|---|
id | string | |
avatars | readonly string[] |
Version history
The panel, its review-tab button and the hooks under them. See Version history.
createVersionHistoryUiExtension
Version history as one UI extension: a review-tab button and the right panel. The panel reads
the enclosing CollabProvider’s routes, or a DocHistoryProvider’s source (the local backend).
Build it once, outside render: uiExtensions must be a stable array.
| Name | Type | Default |
|---|---|---|
options | VersionHistoryPanelProps |
VersionHistoryPanel
The version history right panel: the versions newest first, “Name current version”, and a
read-only preview of any version with “Restore this version”. Reads its history from
DocHistoryProvider, else the enclosing CollabProvider; renders nothing without either.
| Name | Type | Default |
|---|---|---|
__0 | VersionHistoryPanelProps |
Options shared by the panel and createVersionHistoryUiExtension.
| Name | Type | Default |
|---|---|---|
getAuthorName | (authorId: string) => stringDisplay name for a version’s |
DocHistorySource
What the panel and hooks read from. Under a CollabProvider it’s the server’s history routes;
otherwise pass one through DocHistoryProvider.
Where a document’s version history lives: the server’s history routes, or the local backend.
The panel and useDocHistory only talk to this.
History over @stesura/collab-adapter-server’s routes (useCollabDoc() supplies the args).
History over @stesura/collab-adapter-local’s IndexedDB backend.
| Name | Type | Default |
|---|---|---|
__0 | { backend: LocalCollabBackend; docId: string; authorId?: string; } |
DocHistoryProvider / useDocHistorySource
Supplies the version history panel with a DocHistorySource. Only needed where no
CollabProvider is (the local backend: createLocalHistorySource), or to override it. Null
defers to the CollabProvider, if any.
| Name | Type | Default |
|---|---|---|
__0 | { source: DocHistorySource | null; children: ReactNode; } |
The DocHistoryProvider’s source, else the enclosing CollabProvider’s routes, else null.
useDocHistory / useSnapshot
A document’s version history, shared by every caller on the same source. Loads when the first
caller mounts, so opening the panel always shows a fresh list; no polling, since history
changes rarely and a restore reaches the editor as an ordinary commit. saveVersion and
restore refresh the list and reject on failure.
| Name | Type | Default |
|---|---|---|
source | DocHistorySource |
{ refresh: () => Promise<void>; loadMore: () => Promise<void>; saveVersion: (label: string) => Promise<{ version: number; }>; restore: (version: number) => Promise<{ version: number; }>; snapshots: SnapshotMeta[]; status: "error" | ... 2 more ... | "idle"; hasMore: boolean; }What useDocHistory reads.
| Name | Type | Default |
|---|---|---|
snapshots | SnapshotMeta[] | |
status | "error" | "loading" | "ready" | "idle" | |
hasMore | booleanMore (older) versions exist past the loaded ones. |
One version with its document, loaded on first use and cached.
| Name | Type | Default |
|---|---|---|
source | DocHistorySource | |
version | number |
What useSnapshot reads.
| Name | Type | Default |
|---|---|---|
status | "error" | "loading" | "ready" | |
snapshot | Snapshot | null |
Other exports
| Export | Description |
|---|---|
VersionHistoryGroup | The review-tab group with the Version history button, for a custom toolbar. |
VersionPreviewDialog | One version, read-only, with Restore this version behind a confirmation. The panel opens it. |
VERSION_HISTORY_PANEL | "version-history", the right-panel action the panel opens under. |
HISTORY_PAGE_SIZE | 50, versions per page of the history list. |
DocHistoryRequestError | A history request the server refused; carries its status. |
Failure containment
CollabErrorBoundary
Wrap the comments and presence UI (not the editor) in it, so a bad thread or decoration shows a “collaboration unavailable” notice instead of breaking the page. See Failure containment.
Props of CollabErrorBoundary.