> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# 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](https://www.sanity.io/docs/collaboration/sdk-overview).
- Familiarity with [GROQ filters](https://www.sanity.io/docs/content-lake/groq-introduction).

## 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:

**Mentions.tsx**

```tsx
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](https://www.sanity.io/docs/collaboration/sdk-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}`:

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

Replace `YOUR_PROJECT_ID` with your project ID from [sanity.io/manage](https://www.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.

**OpenThreadsAcross.tsx**

```tsx
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](https://www.sanity.io/docs/collaboration/sdk-overview) 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:

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

## Next steps

- [Reference](https://www.sanity.io/docs/collaboration/sdk-overview), for the full signatures of `useCommentsQuery` and `buildCommentThreads`.
- [Show comment counts](https://www.sanity.io/docs/collaboration/show-comment-counts), which counts threads across documents from one query.

