Collaboration (Beta)

Query comments

Query comments across documents with useCommentsQuery and a GROQ filter, and learn what the query reaches.

useCommentsQuery reads comments matching a GROQ filter. It is the escape hatch for anything that is not "comments on this document": a view across several documents, the comments mentioning one user, or activity across the organization. This guide writes a few of those filters and covers what the query reaches and what it costs.

Prerequisites:

  • An app with @sanity/sdk-react 3.11.0 or later set up. See Setup.
  • Familiarity with GROQ filters.

Write a filter

Pass the filter and any parameters it reads. _type == "sanity.comment" is added for you, so the filter only has to say which comments:

import {useCommentsQuery} from '@sanity/sdk-react/collaboration'

function Mentions({userId}: {userId: string}) {
  const {comments} = useCommentsQuery({
    filter:
      'count(message[].children[_type == "mention" && userId == $userId]) > 0',
    params: {userId},
  })

  return <span>{comments.length}</span>
}

The filter runs over the stored comment document, not over the Comment type the hook returns, so it uses the stored field names: target.document._ref, target.path.field, status, message, _createdAt.

The result is a flat list, newest first, replies included. Suspense, isPending, and error work as they do for a document's comments; see Read comments.

Scope to one dataset

The filter runs over every comment the organization can read, not only the ones in the dataset you are reading. Comment queries are routed by organization, and the project and dataset are not sent with them: they choose the organization but do not narrow the results.

To keep to one dataset, add a target.document._ref prefix condition. Refs look like dataset:{projectId}.{dataset}:{documentId}:

const {comments} = useCommentsQuery({
  filter:
    'string::startsWith(target.document._ref, $prefix) && status == "open"',
  params: {prefix: 'dataset:.production:'},
})

Replace with your project ID from sanity.io/manage.

Group the results into threads

To read the list as threads, pass it to buildCommentThreads, which groups comments exactly as useCommentThreads does. Thread first and group by anything else afterwards: a thread needs its replies in the same input as its parent. Each entry in refs is a global document reference, dataset:{projectId}.{dataset}:{documentId}, with the published ID.

import {
  buildCommentThreads,
  useCommentsQuery,
} from '@sanity/sdk-react/collaboration'
import {useMemo} from 'react'

function OpenThreadsAcross({refs}: {refs: string[]}) {
  const {comments} = useCommentsQuery({
    filter: 'target.document._ref in $refs',
    params: {refs},
  })
  const threads = useMemo(
    () =>
      buildCommentThreads(comments).filter(
        (thread) => thread.status === 'open',
      ),
    [comments],
  )

  return <span>{threads.length}</span>
}

Filters that update instantly

Writes show up in a query before the server confirms them, the same as they do on a document's comments. The filter is matched against the written comment locally, so a new comment that answers it appears straight away. That holds for writes made through the resource the query is bound to. Comments written through another dataset arrive once the server confirms them.

Some filters cannot be matched locally, because they need more than the one comment to evaluate:

  • One that dereferences with ->.
  • One that runs a *[...] query of its own.
  • One that calls a context-dependent function such as sanity::dataset(), releases::all(), or now().

Those still work and stay up to date, but only once the server says so. They also cost a connection of their own rather than sharing the one every other list reads from.

For a time window, pass the cutoff as a parameter rather than comparing with now(). Compute it once, in a lazy useState: calling Date.now() during render is impure, and React can discard a memoized value and recompute it:

const [since] = useState(() =>
  new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
)
const {comments} = useCommentsQuery({
  filter: '_createdAt > $since',
  params: {since},
})

Next steps

  • Reference, for the full signatures of useCommentsQuery and buildCommentThreads.
  • Show comment counts, which counts threads across documents from one query.

Was this page helpful?

Sanity logosanity.ioAll Systems Operational© Sanity 2026
Change site theme