Introducing Laravel Quizzes! Play now

Building a like button component with Laravel Livewire

Building a like button component with Laravel Livewire


Today we are going to create the 'like' button you see at the bottom of this post using Laravel Livewire and Tailwind CSS. We will have it restrict clicks first based on user if available, otherwise based on IP address and user agent.

We will create all the needed files:

  • Migration
  • Model
  • Supporting Methods
  • Livewire Component
  • Livewire View

We need to set up some supporting files first that are necessary for this tutorial to work.

Now you can definitely set this up to be morphable and work with multiple models, but for the sake of this tutorial and this site which only needed one 'like' button, I went with a specific model to the resource I was working with.

First we will set up a simple post model and migration:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostsTable extends Migration
{
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->foreignId('user_id')->nullable();
            $table->string('title');
            $table->text('body')->nullable();
            $table->timestamps();
        });
    }
}
<?php

namespace App\Domains\Blog\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected $fillable = [
        'title',
        'body',
    ];

    protected static function booted()
    {
        // We will automatically add the user to the post when it's saved.
        static::creating(function ($post) {
            if (auth()->user()) {
                $post->user_id = auth()->id();
            }
        });
    }
}

Obviously the actual post model has a little more functionality than this, but this is good for the sake of this example.

Next, we will set up the model and migration for the PostLike resource.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreatePostLikesTable extends Migration
{
    public function up()
    {
        Schema::create('post_likes', function (Blueprint $table) {
            $table->id();
            $table->foreignId('post_id');
            $table->foreignId('user_id')->nullable();
            $table->ipAddress('ip')->nullable();
            $table->string('user_agent')->nullable();
            $table->timestamps();
        });
    }
}
<?php

namespace App\Domains\Blog\Models;

use Illuminate\Database\Eloquent\Model;

class PostLike extends Model
{
    protected $fillable = [
        'user_id',
        'ip',
        'user_agent',
    ];
}

Next we need to specify the relationships we need to work with.

We need to add this relationship to both the User and Post models:

use App\Domains\Blog\Models\PostLike;
use Illuminate\Database\Eloquent\Relations\HasMany;

...
public function likes(): HasMany
{
    return $this->hasMany(PostLike::class);
}
...

We are finally up to creating our Livewire component!

We will use the built-in Livewire command to create our new component:

php artisan make:livewire Like

This will create us two new files:

app/Http/Livewire/Like.php
resources/views/livewire/like.blade.php

Our Livewire component will need to accept the current Post as a parameter, and will have a single method called like() that will handle both liking and un-liking:

<?php

namespace App\Http\Livewire\Frontend\Blog\Post;

use App\Models\Post;
use Livewire\Component;

class Like extends Component
{
    public Post $post;

    public function mount(Post $post)
    {
        $this->post = $post;
    }

    public function like(): void
    {
        // TODO
    }

    public function render()
    {
        return view('components.like');
    }
}

Our view will look like this:

<span class="inline-flex items-center text-sm">
  <button wire:click="like" class="inline-flex space-x-2 {{ $post->isLiked() ? 'text-green-400 hover:text-green-500' : 'text-gray-400 hover:text-gray-500' }} focus:outline-none focus:ring-0">
    <svg class="h-5 w-5" x-description="solid/thumb-up" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
        <path d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z"></path>
    </svg>

    <span class="font-medium text-gray-900">{{ $count }}</span>
    <span class="sr-only">likes</span>
  </button>
</span>

This is what it will look like when inactive:

This is what it will look like when active:

There are a few convenience methods we will need that we will create soon.

First, we need to pass through the current like count to the view, this will update automatically when the property is changed thanks to Livewire.

We will use the withCount property on the model to always load the current amount of likes when we retrieve a post:

This goes on the Post model:

protected $withCount = [
    'likes',
];

Then we will pass it through to the view:

<?php

namespace App\Http\Livewire\Frontend\Blog\Post;

use App\Models\Post;
use Livewire\Component;

class Like extends Component
{
    public Post $post;
    public int $count;

    public function mount(Post $post)
    {
        $this->post = $post;
        $this->count = $post->likes_count;
    }
}

As you can see from the view, we have a wire:click="like" on our button, so we need to implement that method:

Our method needs to do a few things:

  1. If the post is already liked, remove that like.
  2. If not, and a user is logged in, add a like to that user.
  3. If no user is logged in, add a like based on the IP and user agent.

Here is what the final method will look like:

public function like(): void
{
    if ($this->post->isLiked()) {
        $this->post->removeLike();

        $this->count--;
    } elseif (auth()->user()) {
        $this->post->likes()->create([
            'user_id' => auth()->id(),
        ]);

        $this->count++;
    } elseif (($ip = request()->ip()) && ($userAgent = request()->userAgent())) {
        $this->post->likes()->create([
            'ip' => $ip,
            'user_agent' => $userAgent,
        ]);

        $this->count++;
    }
}

As you can see if the button is clicked and the post already has a like, we remove that like. If the user is logged in, we add a like to the post using the likes relationship and the current user. If there is no user logged in, we add a like to that post using the current IP address and user agent (If we can ge them from the request).

I extracted out the checking for existing like and remove like functionality to a method on the model for convenience:

public function isLiked(): bool
{
    if (auth()->user()) {
        return auth()->user()->likes()->forPost($this)->count();
    }

    if (($ip = request()->ip()) && ($userAgent = request()->userAgent())) {
        return $this->likes()->forIp($ip)->forUserAgent($userAgent)->count();
    }

    return false;
}

public function removeLike(): bool
{
    if (auth()->user()) {
        return auth()->user()->likes()->forPost($this)->delete();
    }

    if (($ip = request()->ip()) && ($userAgent = request()->userAgent())) {
        return $this->likes()->forIp($ip)->forUserAgent($userAgent)->delete();
    }

    return false;
}

As you can see again, I extracted even more functionality out to reusable scopes for the PostLike model:

public function scopeForPost($query, Post $post)
{
    return $query->where('post_id', $post->id);
}

public function scopeForIp($query, string $ip)
{
    return $query->where('ip', $ip);
}

public function scopeForUserAgent($query, string $userAgent)
{
    return $query->where('user_agent', $userAgent);
}

Here is what our final component looks like:

Here is our final component code:

<?php

namespace App\Http\Livewire\Frontend\Blog\Post;

use App\Domains\Blog\Models\Post;
use Livewire\Component;

class Like extends Component
{
    public Post $post;
    public int $count;

    public function mount(Post $post)
    {
        $this->post = $post;
        $this->count = $post->likes_count;
    }

    public function like(): void
    {
        if ($this->post->isLiked()) {
            $this->post->removeLike();

            $this->count--;
        } elseif (auth()->user()) {
            $this->post->likes()->create([
                'user_id' => auth()->id(),
            ]);

            $this->count++;
        } elseif (($ip = request()->ip()) && ($userAgent = request()->userAgent())) {
            $this->post->likes()->create([
                'ip' => $ip,
                'user_agent' => $userAgent,
            ]);

            $this->count++;
        }
    }

    public function render()
    {
        return view('components.livewire.frontend.blog.post.like');
    }
}
<span class="inline-flex items-center text-sm">
  <button wire:click="like" class="inline-flex space-x-2 {{ $post->isLiked() ? 'text-green-400 hover:text-green-500' : 'text-gray-400 hover:text-gray-500' }} focus:outline-none focus:ring-0">
    <svg class="h-5 w-5" x-description="solid/thumb-up" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
        <pat d="M2 10.5a1.5 1.5 0 113 0v6a1.5 1.5 0 01-3 0v-6zM6 10.333v5.43a2 2 0 001.106 1.79l.05.025A4 4 0 008.943 18h5.416a2 2 0 001.962-1.608l1.2-6A2 2 0 0015.56 8H12V4a2 2 0 00-2-2 1 1 0 00-1 1v.667a4 4 0 01-.8 2.4L6.8 7.933a4 4 0 00-.8 2.4z"></pat>
    </svg>

    <span class="font-medium text-gray-900">{{ $count }}</span>
    <span class="sr-only">likes</span>
  </button>
</span>

I hope you enjoyed this super simple tutorial on creating a Livewire component. Check back soon for more advanced tutorials on Livewire and all things Laravel.

Anthony Rappa

By Anthony Rappa

Hello! I'm a full stack developer from Long Island, New York. Working mainly with Laravel, Tailwind, Livewire, and Alpine.js (TALL Stack). I share everything I know about these tools and more, as well as any useful resources I find from the community. You can find me on GitHub and LinkedIn.