Skip to main content

Voting & Comments Deep Dive

This deep dive covers the internal mechanics of the voting and commenting systems, including optimistic update algorithms, cache management strategies, rating aggregation, cross-component event coordination, and admin moderation workflows.

Architecture Overview

hooks/
use-item-vote.ts # Vote hook with optimistic mutations and cache utilities
use-comments.ts # Comment CRUD hook with rating integration
use-admin-comments.ts # Admin moderation hook with pagination

app/api/items/[id]/
votes/route.ts # GET/POST/DELETE vote endpoints
comments/route.ts # GET/POST comment endpoints
comments/[commentId]/route.ts # PUT/DELETE single comment
comments/rating/route.ts # POST/PUT/GET rating endpoints

lib/db/schema.ts # votes and comments table definitions

Voting System Internals

useItemVote Hook

The hook manages vote state for a single item with full optimistic update support:

interface ItemVoteResponse {
count: number;
userVote: 'up' | 'down' | null;
}

function useItemVote(itemId: string) {
// Returns: voteCount, userVote, isLoading, handleVote, refreshVotes
}

Vote State Machine

The handleVote function implements a toggle-based state machine:

Current StateActionResultNet Change
No voteClick UpUpvote+1
No voteClick DownDownvote-1
UpvotedClick UpRemove vote (toggle off)-1
UpvotedClick DownSwitch to downvote-2
DownvotedClick DownRemove vote (toggle off)+1
DownvotedClick UpSwitch to upvote+2

When the user's current vote matches the requested type, the hook calls unvote() (DELETE). Otherwise it calls vote(type) (POST).

Optimistic Count Calculation

The optimistic update computes the count differential without waiting for the server:

onMutate: async (type) => {
const previousVotes = queryClient.getQueryData(['item-votes', itemId]);
queryClient.setQueryData(['item-votes', itemId], (old) => {
if (!old) return { count: type === 'up' ? 1 : -1, userVote: type };
const countDiff = old.userVote === type ? -1
: old.userVote === null ? 1
: 2; // switching direction
return {
count: old.count + (type === 'up' ? countDiff : -countDiff),
userVote: old.userVote === type ? null : type
};
});
return { previousVotes };
},

The countDiff calculation handles three cases: toggling off (subtract 1), fresh vote (add 1), and switching direction (add 2 for the full swing).

Authentication Gate

Unauthenticated users who attempt to vote are shown a login modal instead of receiving an error:

if (!user) {
loginModal.onOpen('Please sign in to vote on this item');
throw new Error('Authentication required');
}

The error is caught by the mutation's onError handler, which checks for the authentication message and suppresses the error toast.

Query Configuration

staleTime: 1000 * 60 * 5,  // 5 minutes
gcTime: 1000 * 60 * 30, // 30 minutes garbage collection
retry: (failureCount, error) => {
if (error.message.includes('sign in')) return false; // No retry for auth errors
return failureCount < 2; // 2 retries for other errors
},
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 10000),

Vote Cache Utilities

The useVoteCache hook provides cross-component cache operations:

function useVoteCache() {
return {
invalidateAllVotes, // Invalidate all vote queries
invalidateItemVotes, // Invalidate votes for a specific item
clearVoteCache, // Remove all vote data from cache
prefetchItemVotes, // Pre-fetch votes for an item (e.g., on hover)
};
}

Comments System Internals

useComments Hook

The hook provides full CRUD operations with integrated rating support:

interface CreateCommentData {
content: string;
itemId: string;
rating: number;
}

interface UpdateCommentData {
commentId: string;
content?: string;
rating?: number;
}

Return Value

PropertyTypeDescription
commentsCommentWithUser[]Comments with populated user data
isPendingbooleanTrue during initial fetch
createComment(data) => PromiseCreate a new comment
updateComment(data) => PromiseEdit an existing comment
deleteComment(id) => PromiseRemove a comment
rateComment(data) => voidRate a comment
updateCommentRating(data) => voidUpdate an existing rating
commentRatingnumberAggregate rating for the item

Cross-Component Event System

The comment system dispatches custom DOM events for coordination between components that do not share React Query cache keys:

const COMMENT_MUTATION_EVENT = "comment:mutated";

const dispatchCommentEvent = (comment: CommentWithUser) => {
if (typeof window === "undefined") return;
window.dispatchEvent(new CustomEvent(COMMENT_MUTATION_EVENT, { detail: comment }));
};

This allows components like the item detail header (which shows comment count) to react to comment changes without being directly coupled to the comments query.

Rating Aggregation

Comments and ratings are tightly integrated. After any comment mutation (create, update, delete), the hook forces a refetch of the item rating:

onSuccess: async (newComment) => {
queryClient.setQueryData(['comments', itemId], (old = []) => {
// Update cache with new comment...
});
dispatchCommentEvent(newComment);
await queryClient.refetchQueries({ queryKey: ['item-rating', itemId] });
},

This ensures the star rating display updates immediately after a user submits or edits a review.

Query Stability

The comments query uses conservative refresh settings to prevent UI flicker:

staleTime: 2 * 60 * 1000,      // 2 minutes
gcTime: 10 * 60 * 1000, // 10 minutes
refetchOnMount: false, // Don't refetch if data is fresh
refetchOnWindowFocus: false, // Prevent flash on tab switch

Admin Moderation

useAdminComments Hook

The admin moderation hook provides paginated comment management:

function useAdminComments({ page, limit, search }) {
return {
comments: AdminCommentItem[],
totalComments: number,
totalPages: number,
isDeleting: string | null, // ID of comment being deleted
deleteComment: (id: string) => Promise<boolean>,
};
}

Moderation Workflow

  1. Admin navigates to the comments management page.
  2. Comments are displayed with search and pagination.
  3. The isDeleting state tracks which comment is being removed, disabling its row.
  4. Deletion triggers a notification to the comment author via NotificationService.

API Endpoints

MethodEndpointDescription
GET/api/items/:id/votesFetch vote count and user's vote
POST/api/items/:id/votesCast or change a vote
DELETE/api/items/:id/votesRemove a vote
GET/api/items/:id/commentsFetch comments with user data
POST/api/items/:id/commentsCreate a new comment
PUT/api/items/:id/comments/:commentIdUpdate a comment
DELETE/api/items/:id/comments/:commentIdDelete a comment
POST/api/items/:id/comments/ratingRate a comment
PUT/api/items/:id/comments/ratingUpdate a comment rating
GET/api/items/:id/comments/ratingGet aggregate item rating

Feature Flag Integration

Both voting and comments respect feature flags:

const flags = getFeatureFlags();
// flags.ratings -- Controls star rating display
// flags.comments -- Controls comment section visibility

When the database is not configured, these features are automatically disabled.

Accessibility

  • Vote buttons use aria-pressed to indicate the current vote state.
  • The login modal triggered by unauthenticated vote attempts is focus-trapped.
  • Comment forms use proper <label> associations and validation messages.
  • The star rating component supports keyboard navigation with arrow keys.
  • Admin moderation tables include row-level status indicators and keyboard-accessible actions.
  • Loading and error states provide aria-busy and role="alert" attributes respectively.