Reactions
Users react to activities. Counts are denormalized
(reaction_counts) and bumped only when a write actually affected a row — the read path
never runs COUNT(*).
Reacting to someone’s activity creates a notification for its
author and fires a reaction.added webhook.
Add a reaction
Section titled “Add a reaction”// client.reactions.add(kind, activityId, custom?) -> Reactionconst reaction = await client.reactions.add('like', activity.id)import { useReactions } from '@dropinnodex/react'
function LikeButton({ activity }: { activity: { id: string; reaction_counts: Record<string, number>; own_reactions?: string[] } }) { // Seed from the activity you already fetched — this hook manages optimistic // like/unlike state, it does not fetch counts itself. const { react, unreact, counts, ownReactions } = useReactions( activity.id, activity.reaction_counts, activity.own_reactions ?? [], ) const liked = ownReactions.includes('like') return ( <button onClick={() => (liked ? unreact('like') : react('like'))}> {liked ? '❤️' : '🤍'} {counts.like ?? 0} </button> )}Seeding is live, not one-shot: when the activity you pass in comes back with different
counts — someone else liked it and live: true
revalidated the page — the button adopts them. A seed arriving while your own optimistic
write is in flight is skipped, since it was built before your click and adopting it would
visibly undo it; if that write then fails, the rollback lands on the skipped seed rather
than on your pre-click state, so someone else’s like isn’t lost with yours. Render
straight from counts; you never need to remount the button to see another user’s
reaction.
The counter is still last-write-wins against the server. A refresh() issued before a
click can land after it and briefly show pre-write counts; the next revalidation corrects
it. Treat the number as eventually right, not transactional.
There is no server-side reactions.add — a reaction is always attributed to the
authenticated user making the request, and a server token has no
identity, so reactions are created only through the client SDK. To react on a user’s
behalf from a backend, mint a user token for them first.
Remove a reaction
Section titled “Remove a reaction”Two ways to remove: unreact drops the caller’s own reaction of a given kind (no
reaction id needed); delete removes a specific reaction by its id.
// Remove your own reaction of a kind:await client.reactions.unreact(activity.id, 'like')
// Or remove a specific reaction by id (e.g. one returned from reactions.add/list):await client.reactions.delete(reaction.id)const { unreact } = useReactions(activity.id, activity.reaction_counts, activity.own_reactions ?? [])await unreact('like')// Admin-only: deletes any user's reaction by id, bypassing ownership checks.await server.reactions.delete(reaction.id)Read counts
Section titled “Read counts”Reaction counts and the caller’s own reactions arrive denormalized on every Activity
returned from a feed read — no separate request needed:
activity.reaction_counts // Record<string, number>, e.g. { like: 4 }activity.own_reactions // string[] | undefined — kinds the caller has already reacted withList who reacted
Section titled “List who reacted”For the full “who reacted” list (rather than just counts), use client.reactions.list
or the paginated useReactionList hook:
// Page<Reaction>, newest first. `kind` optionally filters server-side.const page = await client.reactions.list(activity.id, { kind: 'like', limit: 20 })import { useReactionList } from '@dropinnodex/react'
const { reactions, loadNext, hasNext, isLoading, remove } = useReactionList(activity.id, { kind: 'like' })// remove(reactionId) optimistically drops it from the list, then calls reactions.deleteconst page = await server.reactions.list(activity.id, { kind: 'like', limit: 20 })The list is keyset-paginated like every other list in the API.
Related
Section titled “Related”- Notifications — where a reaction shows up for the author.
- Activities — the objects being reacted to, and how deleting one affects its reactions.
- Webhooks —
reaction.added/reaction.removeddelivered to your backend.