Write comments
Start threads, reply, edit, delete, resolve, and react with useCommentActions, and handle a failed write.
useCommentActions returns every comment write as one object. This guide starts a thread, replies, edits, deletes, resolves, and reacts, and covers what happens when a write fails.
Prerequisites:
- An app with the SDK set up and a signed-in user. See Setup.
@sanity/sdk-react3.11.0 or later, which adds the@sanity/sdk-react/collaborationexport.- A read on screen to see the writes land. See Read comments.
Get the actions
useCommentActions takes no arguments and returns the write actions. Destructure the ones a component needs:
import {useCommentActions} from '@sanity/sdk-react/collaboration'
function ResolveButton({commentId}: {commentId: string}) {
const {setCommentStatus} = useCommentActions()
return (
<button
onClick={() =>
void setCommentStatus({commentId, status: 'resolved'}).catch(
(error: unknown) => {
// A rejected status change rolls back silently, so say so.
window.alert(`Could not resolve the comment: ${String(error)}`)
},
)
}
>
Resolve
</button>
)
}Every action returns a promise. createComment and replyToComment resolve to the written Comment; the rest resolve to nothing. In a click handler, mark the promise as handled with void, or catch it, so it is not left floating. A rejected status change rolls back silently, so catch it if the user should hear about it.
Write a message
A message is Portable Text, PortableTextBlock[]. There is no composer helper, so wrap plain text yourself:
import type {CommentMessage} from '@sanity/sdk-react/collaboration'
function toMessage(text: string): CommentMessage {
return [
{
_type: 'block',
_key: crypto.randomUUID(),
style: 'normal',
markDefs: [],
children: [{_type: 'span', _key: crypto.randomUUID(), text, marks: []}],
},
]
}Mentions are inline objects of type mention carrying a userId. The SDK stores and returns them untouched, but it has no API for composing one. The CommentInput component from @sanity/collaboration/ui composes them; see Enable mentions.
Start a thread
createComment takes a document handle, optionally the field the thread belongs to, and the message. The handle's resource and perspective decide where it is written; see Resources and perspectives.
const {createComment} = useCommentActions()
await createComment({
documentId: 'post-123',
documentType: 'post',
fieldPath: 'title',
message: toMessage('Can we make this shorter?'),
})It also takes:
commentIdandthreadIdto write against IDs you chose. Each defaults to a new ID, and a newthreadIdstarts a new thread.documentRevisionId, the_revof the document the comment was written about. It is not filled in for you.context, free-form data of your own, handed back on the comment'scontextwherever you read it.- No
fieldPath, to comment on the document as a whole. See Comment on the whole document. anchor, to attach the comment to a run of text. See Anchor inline comments.
Comment on the whole document
A comment can be about the document as a whole rather than one field: a note on its direction, a review sign-off. Leave fieldPath out of createComment, or pass '' or []:
await createComment({
documentId: 'post-123',
documentType: 'post',
message: toMessage('Approved for publishing.'),
})The comment is written without a path and reads back with fieldPath: '', so useCommentThreads({documentId: 'post-123', documentType: 'post', fieldPath: ''}) reads only these. Replies land on the document too, since a reply takes its placement from its parent. An anchor still needs a field: a create with an anchor and no field path rejects with A comment with an anchor needs a field path to anchor it in.
In Sanity Studio with Comments API v2, the comments inspector lists field comments only, so document-level comments don't show there. They're still stored, and every SDK read returns them.
Reply, edit, and delete
replyToComment takes the comment being replied to as parentCommentId. updateComment and removeComment take the commentId of the comment they change:
const {replyToComment, updateComment, removeComment} = useCommentActions()
await replyToComment({parentCommentId, message: toMessage('Done.')})
await updateComment({commentId, message: toMessage('Done, and shortened.')})
await removeComment({commentId})replyToCommentreads the thread, the field, and the document off the parent, which has to be loaded: replying to a comment no read holds rejects. A reply to a reply joins the same thread. It takescommentIdandcontextascreateCommentdoes.updateCommentrewrites the message and stampslastEditedAt.removeCommentdeletes the comment, and its replies too when it starts a thread.
Resolve and reopen
setCommentStatus sets a thread's status to 'resolved' or 'open'. Pass the thread's first comment; its replies follow it:
await setCommentStatus({commentId: thread.parentComment.id, status: 'resolved'})Add and remove reactions
Reactions are two explicit actions rather than a toggle, since a toggle would guess wrong whenever the list is a moment stale:
const {addReaction, removeReaction} = useCommentActions()
await addReaction({commentId, shortName: ':+1:'})
await removeReaction({commentId, shortName: ':+1:'})Both act on the current user's reaction. Adding one that is already there is harmless. shortName is one of a closed set, typed as CommentReactionShortName: ':-1:', ':+1:', ':eyes:', ':heart:', ':heavy_plus_sign:', and ':rocket:'. The API rejects anything else.
Optimistic writes
Every action writes optimistically: the change shows immediately and rolls back if the server rejects it. Render straight from useCommentThreads without tracking pending state.
Creates and replies reach every list they belong in, whichever one you wrote them from, so a document's comments and a query that matches them stay in step.
Rolling back is silent. A rejected edit, delete, status change, or reaction reverts on screen and rejects its promise, and nothing else records it, so catch the promise if the user should hear about it.
Retry a failed create
Creating is the exception to rolling back, and replyToComment counts as creating. A comment that fails to post stays on screen carrying state.type === 'createError' rather than disappearing, so nobody loses what they typed.
Passing the same commentId again retries it:
import {type Comment, useCommentActions} from '@sanity/sdk-react/collaboration'
function Retry({comment}: {comment: Comment}) {
const {createComment, replyToComment} = useCommentActions()
if (comment.state?.type !== 'createError') return null
const retry = () =>
comment.parentCommentId
? replyToComment({
commentId: comment.id,
parentCommentId: comment.parentCommentId,
message: comment.message,
context: comment.context,
})
: createComment({
commentId: comment.id,
threadId: comment.threadId,
documentId: comment.sourceDocumentId,
documentType: comment.documentType,
fieldPath: comment.fieldPath,
message: comment.message,
context: comment.context,
})
return <button onClick={() => void retry()}>Retry</button>
}Pass sourceDocumentId rather than documentId, which is always the published ID: a retry should land on the draft or version the comment was first written against. context is passed again so the retry keeps it.
A retry re-issues createComment or replyToComment with the same comment ID. If the first request reached the server, the API can reject the retry as a conflict.
While a retry is in flight the comment carries state.type === 'createRetrying' instead, so a button keyed on createError steps aside on its own. Key a "Sending…" style on createRetrying.
An inline comment is the one this cannot retry: createComment wants the anchor it was written with, and a comment carries only the resolved selection. Keep the anchor where you wrote it, and pass it again.
Next steps
- Reference, for every option each action takes.
- Comment data flow, for showing a failed write in the components.
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?