PHPackages                             truefanspace/laravel-react-reactions - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Utility &amp; Helpers](/categories/utility)
4. /
5. truefanspace/laravel-react-reactions

ActiveLibrary[Utility &amp; Helpers](/categories/utility)

truefanspace/laravel-react-reactions
====================================

Faceboook like reaction system for Laravel, Inertia and React

1.0.4(5mo ago)0141[4 PRs](https://github.com/TruefansSpace/laravel-react-reactions/pulls)MITPHPPHP ^8.4CI passing

Since Dec 2Pushed 1mo agoCompare

[ Source](https://github.com/TruefansSpace/laravel-react-reactions)[ Packagist](https://packagist.org/packages/truefanspace/laravel-react-reactions)[ Docs](https://github.com/truefanspace/laravel-react-reactions)[ GitHub Sponsors]()[ RSS](/packages/truefanspace-laravel-react-reactions/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (5)Dependencies (14)Versions (14)Used By (0)

Laravel React Reactions
=======================

[](#laravel-react-reactions)

A complete Facebook-like reaction and commenting system for Laravel with Inertia.js and React. Add reactions, comments with nested replies, and toast notifications to any Eloquent model.

Features
--------

[](#features)

- 🎯 **Reactions System**: Facebook-style reactions (like, love, haha, wow, sad, angry) on any model
- 💬 **Comments System**: Full-featured commenting with unlimited nested replies
- 🔄 **Reactions on Comments**: Users can react to comments just like posts
- ⚡ **Inertia.js v2**: Seamless SPA experience with server-side routing
- 🎨 **Beautiful UI**: Animated reaction picker, modal viewers, and smooth interactions
- 🔔 **Toast Notifications**: Built-in toast system for user feedback
- 🔒 **Flexible Permissions**: Customizable permission system for comments
- 🚀 **Query Optimized**: Database subqueries eliminate N+1 problems (1 query for any dataset size)
- 📝 **Full TypeScript**: All React components are written in TypeScript with proper type definitions
- ⚙️ **Configurable**: Reaction types and emojis are fully configurable via config file
- ♿ **Accessible**: Full keyboard navigation and screen reader support
- 🧪 **Fully Tested**: Comprehensive unit, feature, and E2E tests (151 passing tests)

Requirements
------------

[](#requirements)

- PHP ^8.4
- Laravel ^11.0 || ^12.0
- Inertia.js v2
- React 19
- TypeScript ^5.0 (recommended for type safety)

Installation
------------

[](#installation)

```
composer require truefanspace/laravel-react-reactions
```

### Publishing Assets

[](#publishing-assets)

You can publish all package assets at once or individually:

**Publish everything (recommended for first-time setup):**

```
php artisan vendor:publish --provider="TrueFans\LaravelReactReactions\LaravelReactReactionsServiceProvider"
```

**Or publish individually:**

```
# Publish and run migrations
php artisan vendor:publish --tag=react-reactions-migrations
php artisan migrate

# Publish React components (TypeScript)
php artisan vendor:publish --tag=react-reactions-components

# Publish configuration file
php artisan vendor:publish --tag=react-reactions-config
```

**Available tags:**

- `react-reactions-migrations` - Database migrations for reactions and comments tables
- `react-reactions-components` - React/TypeScript components to `resources/js/Components/Reactions`
- `react-reactions-config` - Configuration file to `config/react-reactions.php`

Quick Start
-----------

[](#quick-start)

### 1. Add Traits to Your Model

[](#1-add-traits-to-your-model)

```
use TrueFans\LaravelReactReactions\Traits\HasReactions;
use TrueFans\LaravelReactReactions\Traits\HasComments;

class Post extends Model
{
    use HasReactions, HasComments;

    protected $appends = ['reactions_summary', 'user_reaction'];

    public function canManageComment($comment): bool
    {
        // null = creating new comment, Comment instance = editing/deleting
        if ($comment === null) {
            return auth()->check(); // Anyone can comment
        }

        // Users can manage their own comments
        return $comment->user_id === auth()->id();
    }
}
```

### 2. Setup Controller with Query Optimization

[](#2-setup-controller-with-query-optimization)

```
use Illuminate\Support\Facades\DB;

class PostController extends Controller
{
    public function index()
    {
        $userId = auth()->id();

        // ✅ OPTIMIZED: Load all posts with reactions in 1 query
        $posts = Post::withReactionsData($userId)
            ->latest()
            ->get()
            ->map(fn($post) => [
                'id' => $post->id,
                'title' => $post->title,
                'reactions_summary' => $post->parseReactionsSummary(),
                'user_reaction' => $post->parseUserReaction(),
            ]);

        return Inertia::render('Posts/Index', ['posts' => $posts]);
    }

    public function show(Post $post)
    {
        $userId = auth()->id();

        // ✅ OPTIMIZED: Load all comments with reactions efficiently
        $postIds = [$post->id];

        // Get comment counts in one query
        $commentCounts = Comment::whereIn('commentable_id', $postIds)
            ->where('commentable_type', Post::class)
            ->whereNull('parent_id')
            ->select('commentable_id', DB::raw('count(*) as total'))
            ->groupBy('commentable_id')
            ->pluck('total', 'commentable_id');

        // Load comments with replies in one query
        $comments = Comment::where('commentable_id', $post->id)
            ->where('commentable_type', Post::class)
            ->whereNull('parent_id')
            ->with(['user:id,name,email', 'replies.user:id,name,email'])
            ->withCount('replies')
            ->latest()
            ->take(5)
            ->get()
            ->map(fn($comment) => [
                'id' => $comment->id,
                'content' => $comment->content,
                'user' => $comment->user,
                'user_id' => $comment->user_id,
                'created_at' => $comment->created_at,
                'is_edited' => $comment->is_edited,
                'can_edit' => $userId === $comment->user_id,
                'can_delete' => $userId === $comment->user_id,
                'replies_count' => $comment->replies_count,
                'replies' => $comment->replies->map(fn($reply) => [
                    'id' => $reply->id,
                    'content' => $reply->content,
                    'user' => $reply->user,
                    'user_id' => $reply->user_id,
                    'created_at' => $reply->created_at,
                    'is_edited' => $reply->is_edited,
                    'can_edit' => $userId === $reply->user_id,
                    'can_delete' => $userId === $reply->user_id,
                ]),
            ]);

        return Inertia::render('Posts/Show', [
            'post' => $post,
            'comments' => $comments,
            'total_comments' => $commentCounts->get($post->id, 0),
        ]);
    }
}
```

### 3. Setup Inertia Middleware

[](#3-setup-inertia-middleware)

```
class HandleInertiaRequests extends Middleware
{
    public function share(Request $request): array
    {
        return [
            ...parent::share($request),
            'auth' => [
                'user' => $request->user(),
            ],
            'flash' => [
                'success' => fn() => $request->session()->get('success'),
                'error' => fn() => $request->session()->get('error'),
            ],
            // Share reaction types from config
            'reactionTypes' => config('react-reactions.types', []),
        ];
    }
}
```

### 4. Use React Components

[](#4-use-react-components)

```
import Reactions from '@/Components/Reactions/Reactions';
import Comments from '@/Components/Comments/Comments';

export default function PostShow({ post, comments, total_comments }) {
    return (

            {post.title}

            {/* Reactions */}

            {/* Comments */}

    );
}
```

Query Optimization Guide
------------------------

[](#query-optimization-guide)

### The N+1 Problem

[](#the-n1-problem)

**❌ BAD: This creates N+1 queries**

```
// Loading 10 posts = 21 queries (1 + 10*2)
$posts = Post::latest()->get(); // Appended attributes cause N+1
```

**✅ GOOD: This uses 1 query**

```
// Loading 10 posts = 1 query
// Loading 1000 posts = still 1 query!
$posts = Post::withReactionsData(auth()->id())
    ->latest()
    ->get()
    ->map(fn($post) => [
        'id' => $post->id,
        'reactions_summary' => $post->parseReactionsSummary(),
        'user_reaction' => $post->parseUserReaction(),
    ]);
```

### How It Works

[](#how-it-works)

The `withReactionsData()` scope adds SQL subqueries to aggregate reactions at the database level:

```
SELECT posts.*,
  -- Aggregate all reactions into JSON
  (SELECT JSON_OBJECT(
    'like', COALESCE(SUM(CASE WHEN type = 'like' THEN 1 END), 0),
    'love', COALESCE(SUM(CASE WHEN type = 'love' THEN 1 END), 0),
    ...
  ) FROM reactions
  WHERE reactable_id = posts.id
  AND reactable_type = 'App\\Models\\Post') as reactions_summary_json,

  -- Get current user's reaction
  (SELECT type FROM reactions
  WHERE reactable_id = posts.id
  AND user_id = ? LIMIT 1) as user_reaction_type
FROM posts
```

This executes as **one query** regardless of how many posts you load.

### Performance Comparison

[](#performance-comparison)

DatasetWithout OptimizationWith `withReactionsData()`10 posts21 queries1 query100 posts201 queries1 query1000 posts2001 queries1 query### Comments Optimization

[](#comments-optimization)

**❌ BAD: Loading comments per post in a loop**

```
$posts->map(function($post) {
    $comments = $post->comments()->get(); // N+1 query
    $totalComments = $post->comments()->count(); // Another N+1
});
```

**✅ GOOD: Load all comments at once**

```
$postIds = $posts->pluck('id');

// Get all comment counts in 1 query
$commentCounts = Comment::whereIn('commentable_id', $postIds)
    ->where('commentable_type', Post::class)
    ->whereNull('parent_id')
    ->select('commentable_id', DB::raw('count(*) as total'))
    ->groupBy('commentable_id')
    ->pluck('total', 'commentable_id');

// Load all comments with replies in 1 query
$allComments = Comment::whereIn('commentable_id', $postIds)
    ->where('commentable_type', Post::class)
    ->whereNull('parent_id')
    ->with(['user', 'replies.user'])
    ->withCount('replies')
    ->latest()
    ->get()
    ->groupBy('commentable_id');
```

### Key Optimization Rules

[](#key-optimization-rules)

1. **Always use `withReactionsData()`** when loading multiple models
2. **Eager load relationships** with `with()` to avoid N+1
3. **Use `withCount()`** instead of `count()` in loops
4. **Batch load related data** before mapping
5. **Avoid calling methods on models in loops** that trigger queries
6. **Monitor queries** with Laravel Debugbar in development

Configuration
-------------

[](#configuration)

### Customizing Reaction Types

[](#customizing-reaction-types)

All reaction types are configurable in `config/react-reactions.php`. The frontend automatically reads from this config:

```
return [
    // Customize reaction types and emojis
    'types' => [
        'like' => '👍',
        'adore' => '🥰',  // You can change this to 'love' => '❤️'
        'haha' => '😂',
        'wow' => '😮',
        'sad' => '😢',
        'angry' => '😠',
        // Add your own:
        // 'fire' => '🔥',
        // 'celebrate' => '🎉',
    ],

    'comments' => [
        'reactions_enabled' => true,
        'max_depth' => 3,
        'edit_timeout' => 300, // seconds
        'per_page' => 10,
    ],

    'notifications' => [
        'enabled' => true,
        'admin_email' => env('REACTIONS_ADMIN_EMAIL'),
        'notify_owner' => true,
        'notify_parent_author' => true,
    ],
];
```

**Important**: The reaction types are shared with the frontend via Inertia middleware. Make sure to add this to your `HandleInertiaRequests`:

```
public function share(Request $request): array
{
    return [
        ...parent::share($request),
        'reactionTypes' => config('react-reactions.types', []),
    ];
}
```

This ensures the frontend always uses the same reaction types as the backend, maintaining consistency across your application.

API Reference
-------------

[](#api-reference)

### HasReactions Trait

[](#hasreactions-trait)

```
// Add or update reaction
$model->react(int $userId, string $type): void

// Remove reaction
$model->unreact(int $userId): void

// Get reactions summary
$model->reactionsSummary(): array

// Get user's reaction
$model->userReaction(int $userId): ?string

// Query scope for optimized loading
Model::withReactionsData(?int $userId)

// Parse reactions data (after withReactionsData)
$model->parseReactionsSummary(): array
$model->parseUserReaction(): ?string
```

### HasComments Trait

[](#hascomments-trait)

```
// Add a comment
$model->addComment(int $userId, string $content): Comment

// Get comments relationship
$model->comments(): MorphMany

// Check permissions
$model->canManageComment(?Comment $comment): bool
```

### Comment Model

[](#comment-model)

```
// Add a reply
$comment->addReply(int $userId, string $content): Comment

// Get replies
$comment->replies(): HasMany

// Check permissions
$comment->canEdit(): bool
$comment->canDelete(): bool

// Query scope
Comment::topLevel() // Only top-level comments
```

TypeScript Support
------------------

[](#typescript-support)

All React components are written in TypeScript with full type safety. The package includes:

- ✅ TypeScript definitions for all components
- ✅ Proper type inference for props
- ✅ Type-safe event handlers
- ✅ Configurable reaction types with type safety

### Component Type Definitions

[](#component-type-definitions)

```
// Reactions Component
interface ReactionsProps {
    reactableType: string;
    reactableId: number;
    initialReactions?: Record;
    userReaction?: string | null;
    onUserClick?: (userId: number) => void;
}

// Comments Component
interface CommentsProps {
    commentableType: string;
    commentableId: number;
    initialComments?: Comment[];
    totalComments?: number | null;
    reactionsEnabled?: boolean;
    onUserClick?: (userId: number) => void;
    currentUserId: number;
    perPage?: number;
}

// Comment Type
interface Comment {
    id: number;
    content: string;
    user_id: number;
    user?: User;
    created_at: string;
    is_edited?: boolean;
    reactions_summary?: Record;
    user_reaction?: string | null;
    replies?: Comment[];
}
```

### Using with TypeScript

[](#using-with-typescript)

```
import Reactions from '@/Components/Reactions/Reactions';
import Comments from '@/Components/Comments/Comments';
import type { Comment } from '@/types'; // Define your types

interface Post {
    id: number;
    title: string;
    reactions_summary: Record;
    user_reaction: string | null;
}

export default function PostShow({ post, comments }: {
    post: Post;
    comments: Comment[];
}) {
    return (

    );
}
```

Testing
-------

[](#testing)

```
# Run PHP tests
composer test

# Run E2E tests
npm run build
npm run test:e2e
```

Troubleshooting
---------------

[](#troubleshooting)

### N+1 Query Issues

[](#n1-query-issues)

- Always use `withReactionsData()` when loading multiple models
- Use `with()` to eager load relationships
- Use `withCount()` instead of `count()` in loops
- Monitor queries with Laravel Debugbar

### Reactions Not Working

[](#reactions-not-working)

- Check routes: `php artisan route:list | grep reactions`
- Verify user is authenticated
- Check browser console for errors

### Comments Not Showing

[](#comments-not-showing)

- Verify `initialComments` prop is passed
- Check user authentication
- Ensure `canManageComment()` is implemented

License
-------

[](#license)

MIT License. See [License File](LICENSE.md) for details.

Credits
-------

[](#credits)

- [Vahan Drnoyan](https://github.com/truefanspace)
- Built with Laravel 11, Inertia.js v2, React 19, TypeScript, and shadcn/ui

###  Health Score

43

—

FairBetter than 91% of packages

Maintenance82

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity60

Established project with proven stability

 Bus Factor1

Top contributor holds 82.4% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

5

Last Release

160d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3930a3616e04af3787c975859c0df28830bedfb80796c16ea3c7c5906c29dcf0?d=identicon)[VahanDr.](/maintainers/VahanDr.)

---

Top Contributors

[![VahanDrnoyan](https://avatars.githubusercontent.com/u/23120503?v=4)](https://github.com/VahanDrnoyan "VahanDrnoyan (14 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (2 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

laravelVahan Drnoyanlaravel-react-reactions

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/truefanspace-laravel-react-reactions/health.svg)

```
[![Health](https://phpackages.com/badges/truefanspace-laravel-react-reactions/health.svg)](https://phpackages.com/packages/truefanspace-laravel-react-reactions)
```

###  Alternatives

[spatie/laravel-data

Create unified resources and data transfer objects

1.8k28.9M627](/packages/spatie-laravel-data)[spatie/laravel-livewire-wizard

Build wizards using Livewire

4061.0M4](/packages/spatie-laravel-livewire-wizard)[hirethunk/verbs

An event sourcing package that feels nice.

513162.9k6](/packages/hirethunk-verbs)[worksome/exchange

Check Exchange Rates for any currency in Laravel.

123544.7k](/packages/worksome-exchange)[ralphjsmit/livewire-urls

Get the previous and current url in Livewire.

82270.3k4](/packages/ralphjsmit-livewire-urls)[hydrat/filament-table-layout-toggle

Filament plugin adding a toggle button to tables, allowing user to switch between Grid and Table layouts.

6292.3k1](/packages/hydrat-filament-table-layout-toggle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
