> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sublay.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Comment Section

> CommentSectionProvider setup and the useCommentSection hook reference

`CommentSectionProvider` loads and manages all comment state for a given entity. Descendant components access this state through `useCommentSection`.

## Setting Up CommentSectionProvider

Identify the target entity with one of four mutually exclusive props:

```tsx theme={null}
import { CommentSectionProvider } from "@sublay/react-js";

// By foreign ID (your own system's ID for the content)
<CommentSectionProvider foreignId="your-article-id" createIfNotFound>
  <CommentList />
</CommentSectionProvider>

// By Sublay entity ID (the entity's own `id`, a UUID)
<CommentSectionProvider entityId="0b9d5a3e-6c1f-4f7a-9d2e-8a4c1b7e5f30">
  <CommentList />
</CommentSectionProvider>

// By short ID
<CommentSectionProvider shortId="x7k2m">
  <CommentList />
</CommentSectionProvider>

// By passing the entity object directly (no fetch needed)
<CommentSectionProvider entity={myEntity}>
  <CommentList />
</CommentSectionProvider>
```

### Which entity the section uses

The section can be handed an entity in two ways: the `entity` prop, or, when there's no `entity` prop, the entity of an [`EntityProvider`](/v8/sdk/entities/provider-and-hook) it's rendered inside.

* **It follows that entity as it changes.** A new object for the same entity, such as one with an updated `commentsCount`, updates the `entity` returned by `useCommentSection` and nothing else: the loaded comments, replies and sort stay as they are and nothing is fetched again. Only a different entity `id` resets the section and loads that entity's comments.
* **An id prop decides which entity is wanted.** With `entityId`, `foreignId` or `shortId` set, a handed entity is used only if it is that entity. One that doesn't match is ignored, and the section fetches the entity the id names.
* **A handed entity that goes away is kept.** If the `entity` prop or the `EntityProvider` entity becomes `undefined`, the section keeps showing the last entity it had until it's given another.

## Provider Props Reference

<ParamField path="entityId" type="string">
  Sublay entity ID. Mutually exclusive with `foreignId`, `shortId`, and `entity`.
</ParamField>

<ParamField path="foreignId" type="string">
  Your system's ID for the entity. The section finds the entity that has it; it creates one only with `createIfNotFound`.
</ParamField>

<ParamField path="shortId" type="string">
  Auto-generated short ID.
</ParamField>

<ParamField path="entity" type="Entity">
  A pre-fetched entity object. Skips the entity fetch. The section follows it as it changes: see [Which entity the section uses](#which-entity-the-section-uses).
</ParamField>

<ParamField path="createIfNotFound" type="boolean">
  Used with `foreignId`. Default: `false`. When `true`, the entity is created the first time the section loads if no entity with that `foreignId` exists yet. Without it, the section only finds an existing entity: when there is none, `error` is set and no comments load.
</ParamField>

<ParamField path="limit" type="number">
  Top-level comments per page. Default: `15`.
</ParamField>

<ParamField path="defaultSortBy" type="string">
  Initial sort order: `"top"` (default), `"createdAt"`, or `"controversial"`. Changeable at runtime with `setSortBy`. `"new"`/`"old"` are deprecated aliases for `"createdAt"`.
</ParamField>

<ParamField path="defaultSortDir" type="&#x22;asc&#x22; | &#x22;desc&#x22;">
  Initial sort direction; applies to every sort. Default: `"desc"`. Changeable with `setSortDir`.
</ParamField>

<ParamField path="defaultSortByReaction" type="ReactionType">
  The reaction `"top"` ranks by — the raw count of that reaction. Server default: `"upvote"`. Changeable with `setSortByReaction`.
</ParamField>

<ParamField path="replyDepth" type="number">
  Levels of replies to bring with every page of top-level comments (`0`–`5`). Default: `0` — replies load on demand through [`useReplies`](/v8/hooks/comments/use-replies). See [Nested replies](#nested-replies).
</ParamField>

<ParamField path="replyLimit" type="number">
  Replies per comment in the nested levels. Default: `3`.
</ParamField>

<ParamField path="replySortBy" type="&#x22;createdAt&#x22; | &#x22;top&#x22; | &#x22;controversial&#x22;">
  Sort for replies — both the nested ones and those loaded through `useReplies`. Defaults to the current section sort.
</ParamField>

<ParamField path="replySortDir" type="&#x22;asc&#x22; | &#x22;desc&#x22;">
  Sort direction for replies. Defaults to the current section sort direction.
</ParamField>

<ParamField path="replySortByReaction" type="ReactionType">
  The reaction replies rank by when their sort is `"top"`. Defaults to the section's `sortByReaction` when the section is also sorted by `"top"`, else `"upvote"`.
</ParamField>

<ParamField path="include" type="&#x22;user&#x22; | &#x22;grants&#x22; | (&#x22;user&#x22; | &#x22;grants&#x22;)[]">
  Related data to populate on every comment, at every level — the highlighted comment included. Default: `"user"`.
</ParamField>

<Note>
  The `reply*` props are fixed for the provider's lifetime — changing one reloads the section like any other prop change. Only the section's own sort (`setSortBy`, `setSortDir`, `setSortByReaction`) changes at runtime.
</Note>

<ParamField path="highlightedCommentId" type="string">
  If provided, fetches this comment (and its parent, for a reply) and adds it to the section. Useful for deep-linking to a specific comment. A deleted or removed comment is highlighted as its [placeholder](/v8/data-models/comment#placeholders). If the comment doesn't exist, is on an entity whose author has blocked the user, or belongs to a different entity than the section's, `highlightedComment` stays `null` and the section loads normally.
</ParamField>

<ParamField path="callbacks" type="Record<string, function>">
  Optional callback functions for handling UI interactions:

  * `loginRequiredCallback()` — called when a non-authenticated user tries to comment
  * `usernameRequiredCallback()` — called when the user has no username set
  * `commentTooShortCallback()` — optional. Called when the submitted comment is too short; without it the comment is just not posted
  * `commentCreateFailedCallback({ code?, message?, status?, parentId })` — called when `createComment` sent a comment and the request failed. `code` and `message` come from the server's error response and `status` is its HTTP status — including `user/suspended` with `403` when the user is suspended; all three are `undefined` only when no response arrived (a network error). `parentId` is the comment being replied to, or `null` for a top-level comment. The same details are in [`createError`](#usecommentsection-return-values).
</ParamField>

<ParamField path="mentionTriggers" type="{ user?: string; space?: string }">
  Override the trigger characters for mentions. Defaults: `{ user: "@", space: "#" }`.
</ParamField>

## Nested Replies

By default a section loads top-level comments only, and each comment's replies load on demand. Set `replyDepth` to bring a few levels of replies with every page, so a threaded view renders its first screen with one request:

```tsx theme={null}
import { CommentSectionProvider, useCommentSection, useReplies } from "@sublay/react-js";

<CommentSectionProvider
  entityId={entity.id}
  defaultSortBy="top"
  limit={10}      // top-level comments per page
  replyDepth={2}  // also bring 2 levels of replies
  replyLimit={3}  // up to 3 replies per comment in those levels
>
  <CommentList />
</CommentSectionProvider>;

function CommentList() {
  const { comments, newComments, hasMore, loadMore } = useCommentSection();
  return (
    <>
      {[...newComments, ...comments].map((c) => <CommentThread key={c.id} comment={c} />)}
      {hasMore && <button onClick={() => loadMore()}>More comments</button>}
    </>
  );
}

function CommentThread({ comment }) {
  const { replies, newReplies, hasMore, loadMore, loading } = useReplies({
    commentId: comment.id,
    limit: 10, // direct replies per "load more"
  });

  return (
    <div>
      <CommentBody comment={comment} />
      {[...newReplies, ...replies].map((r) => <CommentThread key={r.id} comment={r} />)}
      {hasMore && (
        <button onClick={() => loadMore()} disabled={loading}>
          {replies.length ? "Load more replies" : "View replies"}
        </button>
      )}
    </div>
  );
}
```

The same `CommentThread` works without `replyDepth`: every comment then starts with no replies loaded, and the first click fetches them. `useReplies` continues from whatever is already loaded and nests on a comment's first load — see [`useReplies`](/v8/hooks/comments/use-replies) for the details.

Each page can return at most 500 comments, nested replies included (`limit × (1 + replyLimit + … + replyLimit^replyDepth)`). The section sends its props as given; a combination over the limit lands in `error`, and a development build logs which values to lower.

Show counts from `repliesCount` (for example "12 replies") and drive "load more" buttons from `hasMore`.

### Blocked authors

When the `moderation` bundle is installed, comments carry two flags you should render (see [Blocked authors](/v8/data-models/comment#blocked-authors)):

* **`isUnavailable`** — show a neutral placeholder row with **no** reply or reaction controls (the server refuses both). Check it before deleted/removed markers.
* **`isAuthorBlocked`** — the user blocked this author; the full comment is there. Collapse it with a "show anyway" option, or hide it — replying and reacting still work.

When the entity's author has blocked the user, the entity itself is not found, so the section has nothing to load.

## Accessing State with `useCommentSection`

```tsx theme={null}
import { useCommentSection } from "@sublay/react-js";

function CommentList() {
  const {
    comments,
    newComments,
    loading,
    hasMore,
    loadMore,
    sortBy,
    setSortBy,
    createComment,
  } = useCommentSection();

  return (
    <>
      <select value={sortBy} onChange={(e) => setSortBy(e.target.value as any)}>
        <option value="top">Top</option>
        <option value="createdAt">New</option>
      </select>
      {loading && <p>Loading...</p>}
      {[...newComments, ...comments].map((c) => (
        <CommentItem key={c.id} comment={c} />
      ))}
      {hasMore && <button onClick={loadMore}>Load more</button>}
    </>
  );
}
```

## `useCommentSection` Return Values

<ResponseField name="entity" type="Entity | null | undefined">
  The entity the section is on. `undefined` until it is known: while the section looks it up from `entityId`, `foreignId` or `shortId`, and when that lookup failed (see `error`). Its `commentsCount` includes a comment that is being posted through `createComment`.
</ResponseField>

<ResponseField name="comments" type="Comment[]">
  Paginated root-level comments (not newly submitted ones).
</ResponseField>

<ResponseField name="newComments" type="Comment[]">
  Comments submitted in this session. Displayed separately from paginated results, sorted newest-first.
</ResponseField>

<ResponseField name="highlightedComment" type="{ comment: Comment; parentComment: Comment | null } | null">
  The highlighted comment (and its parent, if any) when `highlightedCommentId` is set. Both are read from the section, so an edit or delete shows here at once. `null` again when `highlightedCommentId` is cleared or the section moves to another entity.
</ResponseField>

<ResponseField name="highlightedCommentLoading" type="boolean">
  `true` while the comment named by `highlightedCommentId` is being fetched, until it's found, not found, or the fetch fails. `true` again when `highlightedCommentId` changes or the section moves to another entity, until that fetch finishes. `false` when `highlightedCommentId` isn't set. Use it to tell "still loading" apart from "not found" (`highlightedComment` is `null` in both).
</ResponseField>

<ResponseField name="loading" type="boolean">
  `true` while a page of top-level comments is loading, and while the section is looking up its entity. `false` when the entity lookup failed. When the section moves to another entity, `comments` is empty and `loading` is `true` from the first render with that entity.
</ResponseField>

<ResponseField name="error" type="Error | null">
  The last failure loading the section: the comment list, or the entity itself when the section looks it up from `entityId`, `foreignId` or `shortId` and it can't be fetched or isn't found. Cleared when the next load starts. Call `loadMore()` to retry — `hasMore` stays `true` after a failure, and the retry asks for the same page again, including a first page that failed. After a failed entity lookup, `loadMore()` looks the entity up again. A missing highlighted comment doesn't set it.
</ResponseField>

<ResponseField name="hasMore" type="boolean">
  `true` when more top-level comments are available.
</ResponseField>

<ResponseField name="submittingComment" type="boolean">
  `true` while a `createComment` call is in flight.
</ResponseField>

<ResponseField name="createError" type="CommentCreateError | null">
  The last failure creating a comment through `createComment`: `{ code?: string; message?: string; status?: number; parentId: string | null }`. `code` and `message` come from the server's error response (see [Create Comment → Error Responses](/v8/api-reference/comments/create-comment#error-responses)) and `status` is its HTTP status; all three are `undefined` only when no response arrived (a network error). A comment refused because the user is suspended has `code: "user/suspended"` and `status: 403`. `parentId` is the comment that was being replied to, or `null` for a top-level comment. Set when a create fails; cleared when the next create starts and when the section moves to another entity. A create stopped before it is sent — no signed-in user, no username, a comment that is too short — doesn't change it.

  ```tsx theme={null}
  const { createError } = useCommentSection();

  {createError && (
    <p>
      {createError.code === "comment/parent-deleted"
        ? "This comment was deleted, so you can't reply to it."
        : "Couldn't post your comment. Try again."}
    </p>
  )}
  ```
</ResponseField>

<ResponseField name="loadMore" type="() => Promise<void>">
  Loads the next page of top-level comments (with the section's `replyDepth`), or retries the page that failed. When the entity lookup failed, it retries that lookup first; the comments then load on their own once the entity is found. Does nothing while a page is loading or when `hasMore` is `false`, and never throws — failures land in `error`.
</ResponseField>

<ResponseField name="getComment" type="(commentId: string) => Comment | undefined">
  A loaded comment by ID — top-level or a reply — without its nested `replies`.
</ResponseField>

<ResponseField name="sortBy" type="CommentsSortByOptions">
  Current sort: `"createdAt"`, `"top"`, or `"controversial"`. (`"new"`/`"old"` are deprecated aliases for `"createdAt"`.)
</ResponseField>

<ResponseField name="setSortBy" type="(newSortBy: CommentsSortByOptions) => void">
  Changes the sort order and reloads from the first page.
</ResponseField>

<ResponseField name="sortDir" type="&#x22;asc&#x22; | &#x22;desc&#x22;">
  Current sort direction; applies to every sort.
</ResponseField>

<ResponseField name="setSortDir" type="(newSortDir: &#x22;asc&#x22; | &#x22;desc&#x22;) => void">
  Changes the sort direction and reloads from the first page.
</ResponseField>

<ResponseField name="sortByReaction" type="ReactionType | undefined">
  The reaction `"top"` ranks by. Only sent with `sortBy: "top"`.
</ResponseField>

<ResponseField name="setSortByReaction" type="(newReaction: ReactionType | undefined) => void">
  Changes the ranking reaction. While `sortBy` is `"top"` this reloads the section; under any other sort the value is kept for later and nothing reloads.
</ResponseField>

<ResponseField name="createComment" type="(props) => Promise<Comment | undefined>">
  Submits a new comment. Shows it immediately (optimistically) and adds 1 to the parent comment's `repliesCount` and the entity's `commentsCount`, rolling back if the request fails. Accepts:

  * `parentId` — reply to a specific comment ID
  * `content` — text content
  * `gif` — GIF data object
  * `mentions` — array of `Mention` objects
  * `autoReaction` — automatically react with this reaction type after creating (e.g. `"upvote"`)

  Never throws. It resolves to the created comment, or to `undefined` when nothing was posted: no signed-in user, no username (when `usernameRequiredCallback` is set), a comment that is too short, no entity yet, another create still in flight, or a request that failed. When the request fails it also sets `createError` and calls `callbacks.commentCreateFailedCallback`.

  Where the `commentsCount` + 1 goes depends on where the section's entity comes from:

  * **An `EntityProvider` entity** — when it is the section's entity, the provider's entity is updated, so the rest of the page sees the count too.
  * **An entity the section fetched** (`entityId`, `foreignId` or `shortId`) — the section's own copy is updated.
  * **An `entity` prop** — the object you pass is not modified. The count shows on the `entity` that `useCommentSection` returns. Once the comment is posted, it stays until the entity you pass changes its `commentsCount` — up or down — or you pass a different entity; from then on the section shows the count you pass.

  If the section moves to another entity before the request finishes, the created comment is still returned, but nothing is added to the new entity's comments or count.

  The server refuses a direct reply to a comment that was deleted (`comment/parent-deleted`) or removed by a moderator (`comment/parent-removed`). When a reply fails with one of these codes, the section also updates its copy of that parent to the [placeholder](/v8/data-models/comment#placeholders) the server returns for it: a deleted comment gets `userDeletedAt` and its blanked fields; a removed comment gets `moderationStatus: "removed"` and the same blanked fields, or keeps its content when the user is its author.
</ResponseField>

<ResponseField name="updateComment" type="(props: { commentId: string; content: string }) => Promise<void>">
  Updates a comment's text content.
</ResponseField>

<ResponseField name="deleteComment" type="(props: { commentId: string }) => Promise<void>">
  Deletes a comment. It stays in place as a [placeholder](/v8/data-models/comment#placeholders), so the replies under it remain; `repliesCount` and `commentsCount` don't change, since the server keeps counting it.
</ResponseField>

<ResponseField name="selectedComment" type="Comment | null">
  Currently selected/focused comment. Managed via `setSelectedComment`. Cleared when the section moves to another entity.
</ResponseField>

<ResponseField name="setSelectedComment" type="(comment: Comment | null) => void">
  Set or clear the selected comment.
</ResponseField>

<ResponseField name="repliedToComment" type="Partial<Comment> | null">
  The comment currently being replied to. `createComment` uses it as the parent when no `parentId` is passed, and clears it (with the reply banner and `pushMention`) when it sends. If the request fails, `repliedToComment` is put back, so sending again is still a reply to the same comment. Its `user` is then the user the banner named — the pushed mention when there was one, otherwise the comment's author — and `showReplyBanner` is put back when there is such a user. `pushMention` stays cleared. `repliedToComment` is also cleared when the section moves to another entity.
</ResponseField>

<ResponseField name="setRepliedToComment" type="(comment: Comment | null) => void">
  Set the comment to reply to.
</ResponseField>

<ResponseField name="showReplyBanner" type="boolean">
  Whether to show a reply indicator UI banner. Set back to `false` when the section moves to another entity.
</ResponseField>

<ResponseField name="setShowReplyBanner" type="({ newState }: { newState: boolean }) => void">
  Show or hide the reply banner.
</ResponseField>

<ResponseField name="pushMention" type="User | null">
  A user to auto-insert as a mention in the next comment. Set by `handleShallowReply`. Cleared when the section moves to another entity.
</ResponseField>

<ResponseField name="handleDeepReply" type="(comment: Comment) => void">
  Sets the reply target for a deep reply (reply appears nested under the parent comment).
</ResponseField>

<ResponseField name="handleShallowReply" type="(comment: Comment) => void">
  Reply at the same level as `comment`: the reply target becomes `comment`'s own parent, and `comment`'s author is set as `pushMention`. For a top-level comment there is no parent, so the reply is top-level too.
</ResponseField>

## Types

The provider's props and the hook's return value are exported as types, for wrapping the provider or passing the section's values around:

```tsx theme={null}
import type {
  CommentSectionProviderProps, // props of CommentSectionProvider
  CommentSectionContextValues, // what useCommentSection returns
  UseCommentSectionDataProps, // the provider's props without `children`
  UseCommentSectionDataValues, // the section's values
} from "@sublay/react-js";
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.