Add toggle reactions, vote counts, and paginated reactor lists to any Convex resource with a single transactionally-safe edge model and configurable kind vocabu
npm install @vllnt/convex-reactions@vllnt/convex-reactions records a subject’s reaction to an opaque resource reference. Define a fixed reaction vocabulary or allow your own kinds, then read counts and the current subject’s state through Convex queries.
react toggles one author/resource/kind edge. unreact removes it idempotently. counts returns totals by kind; hasReacted and myReactions support active button states; reactors paginates the subjects behind a reaction. Multiple mounts keep separate systems isolated.
Your host resolves identity, authorizes access, and decides whether reaction types may coexist. A toggle is not a safe blind retry: use explicit removal or host idempotency when needed. Moderation, anti-abuse rules, ranking, and UI remain application concerns.
Maintained by [bntvllnt](https://bntvllnt.com) at [VLLNT](https://vllnt.com). Find the author on [GitHub](https://github.com/bntvllnt) and [X](https://x.com/bntvllnt).
Use @vllnt/convex-reactions and call reactions.react(ctx, userId, postId, 'up') or 'down' from a host mutation. The component enforces one edge per author per kind per resource transactionally, so concurrent votes stay accurate. Pass allowedKinds: ['up', 'down'] to the Reactions constructor to reject any other kind at the boundary.
@vllnt/convex-reactions models each reaction as an opaque (authorRef, resourceRef, kind) edge, where kind can be any string such as an emoji. Omit allowedKinds for freeform reactions, then call reactions.counts(ctx, messageId) from a reactive Convex query to get live per-emoji tallies. reactions.reactors(ctx, messageId, kind, paginationOpts) pages who reacted with a specific emoji.
Call reactions.hasReacted(ctx, userId, postId, 'like') from a host query to get a boolean for a single kind, or reactions.myReactions(ctx, userId, postId) to get all kinds the user has placed on that resource. Both are reactive Convex queries and update automatically when the underlying reaction data changes.
Use reactions.reactors(ctx, resourceRef, kind, paginationOpts) from a host query. It returns a PaginationResult compatible with Convex's usePaginatedQuery, ordered oldest first. The host wraps this in its own re-exported query so it controls auth before exposing reactors to the client.
@vllnt/convex-reactions is auth-agnostic. The host application resolves the user identity and passes an opaque authorRef string to the component's react, hasReacted, and related methods. The component never reads session state or touches host tables, so it works with any auth strategy the host uses.
Each toggle is executed as a single Convex mutation transaction. The component enforces uniqueness of the (authorRef, resourceRef, kind) edge within that transaction, so concurrent react calls cannot produce duplicate edges or incorrect counts. The counts query reflects the transactionally correct state.
Yes. Pass allowedKinds as an array of strings when constructing the Reactions instance, for example allowedKinds: ['up', 'down'] or a fixed emoji list. Any kind not in that list is rejected at the component boundary before any database write occurs. Omit allowedKinds entirely to allow freeform kinds.
Run npm install @vllnt/convex-reactions (peer dependency: convex@^1.41.0), then import the convex.config and call app.use(reactions) in convex/convex.config.ts. Instantiate the Reactions class with components.reactions in any host mutation or query file and call its methods from your own Convex functions.
@vllnt/convex-reactions is a backend-only component with no ./react entry point. Reaction tallies and reactor lists are consumed on the frontend by calling useQuery against the host's own re-exported counts and reactors query functions, which is the standard Convex reactive query pattern.