Advanced Livewire: Abstracting notifications

By @samuel · 2021-10-18 19:32

Advanced Livewire: Abstracting notifications

When implementing features such as toast notifications, the most common approach in Livewire is to dispatch a browser event from the component:

$this->dispatchBrowserEvent('toast-notification', 'Your changes were saved.');

And then listen to that event in Alpine:

<div
    x-data="{ ... }"
    @toast-notification.window="show($event)"
>...</div>

This works, but it has two disadvantages:

  • The code is a bit too low level, it'd be nicer to have a more expressive method in Livewire
  • We cannot dispatch notifications from outside Livewire components

Let's start by fixing the first one.

Creating a trait

The simplest way to abstract some logic in Livewire is to create a trait. In this case, the trait would look something like this:

trait WithNotifications
{
    protected function notify(string $message): void
    {
        $this->dispatchBrowserEvent('toast-notification', $message);
    }
}

And now we can use it in any component like this:

class FooComponent extends Component
{
    use WithNotifications;

    public function bar()
    {
        // ...

        $this->notify('Your changes were saved');
    }
}

That's a lot cleaner!

Of course, the difference isn't that significant here, since it's a simplified example. But in the real world you might pass multiple arguments to the method (for instance the notification title, body, etc).

Now let's address the second issue.

Using a separate class

It'd be cool if we could dispatch these notifications from other places as well. There aren't many obvious use cases, but using a separate class will let us:

  • Use these features without having to add traits to all components
  • Interact with the browser from other places, e.g. show a notification from an action class, when some cache is pruned, etc

Let's start by creating the class API that we want to use:

class Toast
{
    public array $notifications = [];

    public static function flash(string $message): void
    {
        static::$notifications[] = $message;
    }
}

I'm using a static class since it requires less work compared to a normal class registered as a singleton that has a separate facade class. This lets us call just:

Toast::flash('Your changes were saved!');

Pretty clean! But this doesn't actually do anything. So let's tell Livewire to actually read this data and add it to the browser notifications:

Livewire::listen('component.dehydrate', function ($component, $response) {
    foreach (Toast::$notifications as $notification) {
        $response->effects['dispatches'] ??= [];

        $response->effects['dispatches'][] = [
            'event' => 'toast-notification',
            'data' => $notification,
        ];
    } 
});

Now we just need to find a way to make this code get executed. One approach would be adding it to the AppServiceProvider, but that's a bit ugly because it spreads the toast notification logic over multiple files.

So rather than doing that, we can make the event hook self-registering. We'll simply call the code above — once — when the notification functionality is actually made use of.

class Toast
{
    public array $notifications = [];

    protected static bool $registered = false;

    public static function flash(string $message): void
    {
        static::register();

        static::$notifications[] = $message;
    }

    protected static function register(): void
    {
        if (static::$registered) {
            return;
        }

        Livewire::listen('component.dehydrate', function ($component, $response) {
            foreach (static::$notifications as $notification) {
                $response->effects['dispatches'] ??= [];

                $response->effects['dispatches'][] = [
                    'event' => 'toast-notification',
                    'data' => $notification,
                ];
            } 
        });

        static::$registered = true;
    }
}

And that's it for this post. In the next article, we'll go over custom response effects.

They're an alternative solution to the problem described in this article, so we'll go over how you can make use of them instead of simple browser events like we've done here.

Hope you've found this useful, and I'll see you next week!

  • By @yoeunes · 2021-10-24 18:57

    Hello great article here, but I think the 'Livewire::listen' part should be in a service provider boot method instead, that's much cleaner, and you do not have to call for every flash action

    • By @samuel · 2021-10-24 19:08

      should be in a service provider

      The article mentions that it can be there. But it goes to explain why it's placed inside the class instead — to make it self-registering.

      do not have to call for every flash action

      It doesn't. Note what static::$registered is used for in the code.

      • By @yoeunes · 2021-10-24 19:13

        Ah my bad I didn't spot that, yes you're right thanks. Great article, congrats

  • By @geovanek · 2021-10-21 12:03

    Hello, Sorry for my low level of knowledge. Where is this Toast class located? And how do I use it on components?

    • By @samuel · 2021-10-21 12:26

      You can place the class anywhere — it's a static class so you can just import it and call the methods in any file.