Client

Client-side implementation of DocSync

createDocSyncClient

Creates a DocSync client instance along with React hooks for document synchronization.

import { createDocSyncClient } from "@docukit/docsync-react/client";

const { client, useDoc, usePresence } = createDocSyncClient({
  docBinding,
  server: { url: "http://localhost:8080", auth: { mode: "request" } },
  local: { provider: indexedDBProvider },
  timing: { collabMaxDebounce: 50, singleClientMaxDebounce: 3000 },
});

For browser apps with HttpOnly session cookies, auth: { mode: "request" } is the recommended path. The server authenticates from the handshake request, and JavaScript does not need to read the session secret. Use token auth for non-browser clients, tests, workers, server-to-server sync, API keys, and scoped document grants.

Props

Prop

Type

Returns

Prop

Type


getDocObserver

Creates a lazy observer for imperative code. Calling getSnapshot() does not load the document. The first subscriber starts the query, and removing the last subscriber releases it. One observer can have multiple subscribers while using only one underlying document subscription.

const observer = client.getDocObserver({
  type: "notes",
  id: "doc-123",
  createIfMissing: true,
});

const render = () => {
  const result = observer.getSnapshot();
  if (result.status === "success") console.log(result.data.doc);
};

const unsubscribe = observer.subscribe(render);
render();

// Later, when this consumer no longer needs the document:
unsubscribe();

getSnapshot() returns the same object until the query state changes. This is the contract expected by external-store integrations such as React's useSyncExternalStore; useDoc uses this observer internally.

getSnapshot() reports the state of the document itself, not of this observer. A document is shared by every consumer in the client, so an observer that has not subscribed yet already reports whatever another consumer has loaded for the same id — which is what lets a second component render a loaded document on its very first render instead of flashing pending. The same rule applies after unsubscribe(): the snapshot keeps reporting the document while another consumer holds it, and reports the last value it saw once nobody does. Only while at least one subscriber is active is the snapshot guaranteed to track a document that is still loaded, so read it outside a subscription for display only, and subscribe before using data.doc.


useDoc

React hook that subscribes to a document with reactive state updates.

const result = useDoc({ type: "notes", id: "doc-123" });

const result = useDoc({ type: "notes", id: "doc-123", createIfMissing: true });

Props

Prop

Type

Returns

Prop

Type

status describes the available result. fetchStatus answers a different question: can the query serve the authoritative document right now? Because the two are independent, local or stale data stays available while synchronization is paused or a later sync has failed.

SituationstatusfetchStatusResult
Loading for the first time, or resyncing after a reconnectpending, success, or errorfetchingExisting local data and errors are preserved
Temporarily offline or manually disconnectedpending, success, or errorpausedExisting local data and errors are preserved
Document available and up to datesuccessidleServer result is available, including undefined when an optional document does not exist
A routine background sync is in flightunchangedunchangedNothing is emitted: the document on screen is already correct
Local storage failederroridle or pausederror contains the local failure; a disconnected query remains paused
Authentication or permanent connection rejectionerrorpausederror contains the connection failure
Server permanently rejected a syncerroridleExisting data is preserved and error contains the authorization or validation failure
Network or database attempt failed and is retryingunchangedunchangedThe query result remains unchanged while DocSync retries

Background syncs do not report fetching

DocSync is realtime over a persistent socket and applies your edits optimistically, so while a push travels to the server the document you are rendering is already correct. Remote operations coming back from other clients are applied to the same live document instance. Neither produces a new query result, so a document that is loaded and connected simply stays success / idle through all of it. Only a wholesale document replacement changes data, and that is rare.

The alternative was considered and rejected: reporting every push as idle → fetching → idle, the way TanStack Query reports a background refetch. It overloads fetchStatus with a second meaning applications cannot act on, and it emits two new query results per sync — at the 50ms collaborative debounce, roughly 40 re-renders per second of everything under useDoc, for a document that never changed.

Want a "Saving…" indicator like Google Docs? Build it from the client's sync event, which fires once per sync with the request and its outcome. That is the signal such an indicator actually needs, and keeping it out of fetchStatus costs nothing to applications that do not want one.

const [saving, setSaving] = useState(false);

useEffect(() => {
  if (!client) return;
  return client.on("sync", ({ error }) => {
    setSaving(false);
    if (error) console.warn("sync failed", error);
  });
}, []);

Because data and a terminal error can exist at the same time, applications should keep rendering usable data and show the later error separately. A paused state is often transient, so applications may delay a connectivity notice to avoid a flash during short interruptions.

Retries are not unlimited. While the current socket connection remains active, a failed sync request or a DatabaseError response is retried with exponential backoff for about 24 seconds. Failed attempts during that backoff do not change the query result at all. After the retry budget is exhausted, the query reports error and keeps its data. At that point error means DocSync has stopped trying on its own.

That means a loaded document whose background syncs start failing keeps reporting success for up to 24 seconds before the failure reaches QueryResult. Nothing is lost — the edits were already written to local storage — but if you want to react sooner, listen to the sync event, which fires on every attempt including the failed ones.

A transport disconnect cancels that per-document retry and moves the query to paused. Socket.IO then owns reconnecting the transport. When the connection returns, DocSync resumes the document sync. The retry budget covers one episode of failure, so a later edit gets its own bounded chain rather than failing on its first attempt. The package README contains the complete error and retry policy, including permanent connection failures and errors raised by configured local providers or document bindings.


usePresence

React hook that subscribes to presence updates for a document and provides a setter function.

const [presence, setPresence] = usePresence({ docId: "doc-123" });

setPresence({ cursor: { x: 100, y: 200 }, color: "#ff0000" });

Props

Prop

Type

Returns

Prop

Type

Change Origins

DocSyncClient emits a change event whenever a loaded document changes:

client.on("change", ({ docId, origin, operation }) => {
  console.log(docId, origin, operation);
});

origin identifies where the operation came from:

  • "local": a change made by this client in this document instance.
  • "network": a change received from the DocSync server, usually from another device.
  • "local-broadcast": a change received through local broadcast from another tab or window on this device.

On this page

Edit on GitHub