Sanity React App SDK v2.19.0: Presence now works in both directions

Until this release, an app could see other people in a document but never appeared to them, so a Studio user editing alongside your app saw nobody. Your app can now announce its own users, read presence for a single document rather than the whole dataset, and interoperate with the Studio's navbar and field indicators. Presence also survives network interruptions, and participants who disappear no longer linger.

Both @sanity/sdk-core and @sanity/sdk-react are published at 2.19.0. There are no breaking changes: existing usePresence callers keep working, and they don't start announcing on upgrade.

Report presence from your app

useReportPresence is the new write side. Reporting is opt-in per call site, so nothing announces unless you ask for it.

import {useReportPresence} from '@sanity/sdk-react'

// Document level: "I am in this document"
useReportPresence({documentId, documentType})

// Field level: "I am in the title field"
useReportPresence({documentId, documentType, path: ['title']})

path accepts keyed and numeric segments, so you can address array items and Portable Text spans. Pass selection to report a Portable Text caret. You can report carets today, but there's no hook yet for turning remote selections into rendered ones. That arrives with a Portable Text plugin built on this release.

Read presence for a single document

usePresenceForDocument returns only the participants in one document:

const {presence} = usePresenceForDocument({documentId, documentType})
const inTitle = usePresenceForDocument({documentId, documentType, path: ['title']})

Each participant is {user, sessionId, documentId, path, lastActiveAt, selection?}.

By default, a draft, its published document, and any release versions count as the same document, which is what a document list wants. Pass excludeVersions: true to compare ids exactly, which is what a field indicator wants.

Studio interoperability

Your app and the Studio share the same presence room and wire format, so both directions work without configuration. Your app's users appear in the Studio's navbar and field indicators, and Studio users appear in your app.

Field-level interoperability depends on the perspective, because the Studio's field indicators compare the exact document id its form is on. Pass the plain document id and let the perspective decide, either from ResourceProvider or on the handle:

// Reports the draft, which is what the Studio edits by default
useReportPresence({documentId, documentType})

// Reports a release version
useReportPresence({documentId, documentType, perspective: {releaseName: 'autumn'}})

Things to know

Presence is scoped to a project and dataset, not to your organization. It answers who else is in this Document, not who is online at your company.

Participants are counted by session. One person in two tabs appears twice, with two sessionId values and the same user.id. Group by user.id for one avatar per person.

lastActiveAt comes from the sender's clock, so use it for display only. Expiry is handled for you, and a skewed clock will report the wrong time.

New exports

@sanity/sdk-react: useReportPresence, UseReportPresenceOptions, usePresenceForDocument, and UsePresenceForDocumentOptions.

Notable bugfixes and improvements

Presence survives a dropped connection. Reconnects use exponential backoff, and the inbound subscription is rebuilt with the socket instead of leaving an empty participant list behind.

Participants who vanish are cleaned up. Sessions expire after 90 seconds without a heartbeat, and the local user disconnects explicitly on beforeunload and pagehide, which covers iOS Safari and the back/forward cache.