Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Reactions Laravel Package

hkp22/laravel-reactions

Add reactions such as likes, loves, and more to any Eloquent model in your Laravel app. The package offers polymorphic, customizable reaction types, simple model traits, and a clean API for attaching, toggling, and counting reactions. Great for posts, comments, and social features.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and scaffold the package in three commands:

composer require hkp22/laravel-reactions
php artisan vendor:publish --provider="Hkp22\LaravelReactions\LaravelReactionsServiceProvider"
php artisan migrate

The provider publishes a config/reactions.php (reaction-type registry) and a migration creating the polymorphic reactions table. Skim the config first — it's the single knob that controls which reaction types are legal, and every error you'll hit later ("invalid reaction type") is really a config lookup that missed.

Wire the two traits:

// app/Models/User.php
use Hkp22\LaravelReactions\Traits\Reacter;

class User extends Authenticatable
{
    use Reacter;
}

// app/Models/Post.php
use Hkp22\LaravelReactions\Traits\Reactable;

class Post extends Model
{
    use Reactable;
}

Your first use case — a "like" button — is a four-line controller:

public function react(Post $post)
{
    auth()->user()->toggleReactionTo($post, 'like');
    return back();
}

That's the whole vertical slice. If you can get toggleReactionTo returning true/false in a test, the rest of the package is variations on the same call.

Implementation Patterns

React / unreact / toggle. The Reacter trait gives you three primitives. toggleReactionTo() is what you want for UI buttons because it's idempotent from the caller's perspective — a double-click can't create duplicate rows if your migration has the right unique index. reactTo() and unreactTo() are for explicit actions (e.g., a moderation tool that force-removes a reaction).

Gate rendering on state. Before drawing a button, check whether the current user already reacted so you can render the "active" style:

$reacted = auth()->user()->hasReactedTo($post, 'like');

For a page listing many posts, do not call this per row inside Blade. Instead eager-load and use withCount with a filtered closure:

$posts = Post::withCount([
    'reactions as likes_count'    => fn ($q) => $q->where('reaction_type', 'like'),
    'reactions as dislikes_count' => fn ($q) => $q->where('reaction_type', 'dislike'),
])->paginate(20);

Then derive the per-user state either from a second query (->whereIn('reactable_id', $posts->pluck('id')) on the reactions relation for auth()->id()) or cache it. This is the single biggest performance pattern with any polymorphic reaction table.

Aggregate summaries for detail pages. reactionSummary() returns counts keyed by type and is fine for a single post. Cache the result (Redis, 60s) on high-traffic pages — it's a GROUP BY on a table that grows fast.

Polymorphic by design. Add Reactable to Comment, Video, Review and the same reactTo() call works. The trait stores reactable_type + reactable_id, so no schema change is needed when you add a new reactable model. This is where the package earns its keep versus a hand-rolled likes table.

Custom reaction types. The config maps a string key to a reaction class:

// config/reactions.php
'reactions' => [
    'like'    => \Hkp22\LaravelReactions\Reactions\Like::class,
    'love'    => \Hkp22\LaravelReactions\Reactions\Love::class,
    'dislike' => \Hkp22\LaravelReactions\Reactions\Dislike::class,
    'fire'    => \App\Reactions\Fire::class, // your own
],

Subclass the package's base Reaction for Fire and register it. Once registered, reactTo($post, 'fire') just works and reactionSummary() includes it automatically.

Switching reaction types. A user who liked a post and now wants to love it: the package treats those as two different reactions. The clean pattern is unreactTo($post, 'like') then reactTo($post, 'love') — or wrap it in an application service so controllers stay thin and the operation is transactional.

API responses. Shape the payload explicitly rather than dumping the relation:

return response()->json([
    'reacted'  => $user->hasReactedTo($post, 'like'),
    'summary'  => $post->reactionSummary(),
    'count'    => $post->reactions()->count(),
]);

Gotchas and Tips

  • Add the unique index yourself. Check the published migration. You want a unique constraint on (reacter_id, reactable_id, reactable_type, reaction_type) — without it, concurrent requests can insert duplicate rows and reactionSummary() inflates. If the migration is missing it, add it in a follow-up migration rather than editing the vendor file.
  • Type strings are the contract. A typo in 'liek' throws at runtime, not at compile time. If your reaction types are user-facing, promote the config keys to a PHP enum (or at least class constants) and validate incoming payloads with Rule::in(array_keys(config('reactions.reactions'))).
  • toggleReactionTo toggles the same type only. It will happily create a second row if the user previously reacted with a different type. Don't assume it means "clear all reactions." For a "one reaction per user" UI, unreact first.
  • No cascade delete by default. Deleting a Post leaves orphaned reaction rows unless you either add a foreign key with onDelete('cascade') (won't work cleanly with polymorphic types in all DBs) or clean up in a model deleting event / observer. Left unattended, the table bloats with dead references.
  • Config caching. After editing config/reactions.php, run php artisan config:clear (and config:cache in deploy). A stale cached config will reject your new reaction type with a confusing error.
  • Eager-load reactions when you iterate. $post->reactions()->count() in a loop is N+1. Use withCount or load('reactions').
  • Test with RefreshDatabase. The package's own test suite relies on migrations running cleanly; mirror that. Assert both the database row (assertDatabaseHas('reactions', [...])) and the trait query (assertTrue($user->hasReactedTo($post))) — the two going out of sync is the most common regression when you refactor.
  • Extend through reaction classes, not the trait. If you need extra data on a reaction (weight for ranking, metadata JSON), subclass the base Reaction class and override its hooks rather than copy-pasting the trait into your model. The package is small enough that reading src/Reactions/ takes ten minutes and pays for itself the first time you need a custom type.
  • Version note. The last release is dated 2026-04-27, which is unusual (likely a metadata oddity or a forward-dated tag on Packagist). Pin to an explicit ^ constraint that you've tested, and check the CHANGELOG diff between whatever you pulled and the previous tag before upgrading in production.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package