Comments (React)
The comments UI. @stesura/comments-react has the components and commentsUiExtension; the
CommentAdapter type, CommentProvider and hooks come from @stesura/editor-react (/types and
/providers), with CommentProvider and useCommentCtx re-exported by @stesura/comments-react.
For setup, the document model and commands see Comments.
CommentAdapter
Your storage backend, passed to <CommentProvider value={adapter}>. Import the type from
@stesura/editor-react/types. Only useThreads and createThread are required; each optional
method enables the matching UI (no resolveThread, no Resolve button). None is called in view mode.
| Member | Signature | Description |
|---|---|---|
useThreads | (query: { resolved: boolean; editorId: string }) => { threads: Thread[]; isLoading: boolean } | Required. React hook returning one editor’s threads. |
createThread | (params: { body: JSONContent; metadata: Record<string, unknown> }) => Promise<Thread> | Required. Creates a thread with its first comment. metadata carries { editorId }. |
addComment | (params: { threadId; body }) => void | Adds a reply. |
editComment | (params: { threadId; commentId; body }) => Promise<void> | Edits a comment. |
deleteComment | (params: { threadId; commentId }) => void | Soft-deletes a reply (sets deletedAt). |
undoDeleteComment | (params: { threadId; commentId }) => void | Clears deletedAt. |
deleteThread | (threadId: string) => Promise<void> | Soft-deletes a thread (sets deletedAt). Also called for orphaned threads. |
undoDeleteThread | (threadId: string) => Promise<void> | Clears deletedAt. Also called for restored threads. |
resolveThread | (threadId: string) => Promise<void> | Marks a thread resolved. |
unresolveThread | (threadId: string) => Promise<void> | Reopens it. |
addReaction | (params: { threadId; commentId; emoji }) => void | Adds the current user’s reaction. |
removeReaction | (params: { threadId; commentId; emoji }) => void | Removes it. |
getThreadsForExport | (editorId: string, state: EditorState) => Promise<CommentsDocx[]> | Non-hook fetch used by DOCX export. |
currentUserId | string | The current user: only their own comments can be edited or deleted, and their reactions show as active. Without it nothing can be edited or deleted. |
Contract
useThreadsis called twice per editor, withresolved: falseandresolved: true, and the results are merged. Return only threads whoseresolvedmatches exactly and that belong toeditorId(store themetadata.editorIdyou get increateThread). Returning everything from both calls shows every thread twice.- Keep returning soft-deleted threads with
deletedAtset. The UI hides them, but needs them to undo a deletion and to restore a thread when its anchors come back. - Report
isLoadinghonestly. Orphan cleanup waits until both calls have loaded: a partial list makes live threads look unknown, and their anchors would be removed. - Reject on failure. The UI waits for the promise-returning methods: a failed edit keeps the draft in the composer, and success toasts (with Undo) only show once the call resolves.
- Normalize before modifying. An adapter that reads, modifies and writes back a stored thread
must run
normalizeSerializedThread(from@stesura/comments) on what it reads. The reaction helpers assume one group per emoji and one entry per user.
Authorization is the adapter’s job
The UI only offers edit and delete to a comment’s author (currentUserId), and deleting a thread’s
first comment deletes the whole thread. These are affordances, not security: re-check who is
asking on the server, for every mutation. Moderator rules, for example, belong there.
Adapter data is untrusted
In a shared document, thread bodies, author names, avatars and mention IDs are written by other users:
- Mention IDs reach
resolveUsers({ userIds })verbatim. Encode them before putting them in a URL, query or filter. - Avatar URLs are sanitized (
javascript:and non-rasterdata:URLs fall back to initials), but any web URL is still fetched by every viewer. Filter them in the adapter if that matters. - Comment bodies are validated against the comment schema; a body that doesn’t parse is shown as unavailable, never rendered as markup.
Hooks
useCommentCtx()
Returns the adapter, or null without a provider. For calling adapter methods directly.
import { useCommentCtx } from "@stesura/editor-react/providers";
const adapter = useCommentCtx();useComments(options?)
import { useComments } from "@stesura/editor-react/providers";
const { threads, isLoading } = useComments({ resolved: false });
const { threads: intro } = useComments({ resolved: true, editorId: "intro" });Calls the adapter’s useThreads. Safe without a provider. threads: undefined means no adapter or
not loaded yet, not zero threads.
| Option | Type | Default | Description |
|---|---|---|---|
resolved | boolean | false | Which set to read (exact match). |
editorId | string | the current editor | Which editor’s threads to read. |
UI components
Exported from @stesura/comments-react:
| Export | Purpose |
|---|---|
commentsUiExtension | Registers everything below plus the toolbar groups (Insert, Review), the text-menu button and the context-menu items. |
ThreadsContainer | The comments root: the rail or floating panel, the composer, screen-reader announcements, and the sync between adapter, stores and document (visible threads, orphan cleanup). Renders nothing without an adapter. |
AnchoredThreads | The rail: thread cards next to the page, aligned with their anchors. |
FloatingThreads | The selected thread in a popover under its anchor. |
FloatingComposer | The new-comment composer, shown next to the pending range or node. |
The display mode is the showComments user preference, set from the Review tab: anchored (rail),
minimized (rail of bubbles), floating or none. The rail modes aren’t available in the compact
layout.
Render the components yourself only in a custom editor shell. ThreadsContainer must stay
mounted: without it no thread is marked visible and nothing is highlighted.
Author names, avatars and mention suggestions come from resolveUsers and
resolveMentionSuggestions on StesuraUserProvider, see
Getting started.
Implementing your own adapter
An in-memory adapter:
import type { Thread } from "@stesura/core/types";
import type { CommentAdapter } from "@stesura/editor-react/types";
import { useMemo, useState } from "react";
function useMemoryCommentAdapter(currentUserId: string): CommentAdapter {
const [threads, setThreads] = useState<Thread[]>([]);
return useMemo(() => {
const update = (id: string, change: Partial<Thread>) =>
setThreads((all) => all.map((t) => (t.id === id ? { ...t, ...change } : t)));
return {
currentUserId,
useThreads: ({ resolved, editorId }) => {
const matching = useMemo(
() =>
threads.filter((t) => t.resolved === resolved && t.metadata.editorId === editorId),
[threads, resolved, editorId]
);
return { threads: matching, isLoading: false };
},
createThread: async ({ body, metadata }) => {
const id = crypto.randomUUID();
const now = new Date();
const thread: Thread = {
id,
resolved: false,
createdAt: now,
metadata,
comments: [
{ id: crypto.randomUUID(), threadId: id, createdAt: now, body, author: { id: currentUserId } },
],
};
setThreads((all) => [...all, thread]);
return thread;
},
deleteThread: async (id) => update(id, { deletedAt: new Date() }),
undoDeleteThread: async (id) => update(id, { deletedAt: undefined }),
resolveThread: async (id) => update(id, { resolved: true }),
unresolveThread: async (id) => update(id, { resolved: false }),
};
}, [threads, currentUserId]);
}For collaboration, use createCollabCommentAdapter from @stesura/collab-adapter-react instead,
see Collaboration.
Next steps
- Comments: setup, anchors, plugins and commands.
- Collaboration: backends and sync.