Advanced Livewire: Traits

By @samuel · 2021-08-31 21:29

Advanced Livewire: Traits

Hey everyone,

I figured that I'd make a "series" of Livewire tips which would basically teach some advanced development patterns, since I feel like a lot of people don't see the true power of Livewire.

This is something I'd like to write about in a longer blog post, or even an ebook, but I'll just post these things on the forum because it's a lot more efficient.

  • The forum lets me share things in a way that doesn't take much time. I can just write one thread at a time, without worrying about the structure too much
  • People see it immediately, without having to wait for me to finish that perfectly polished blog post. So it helps people earlier
  • I get faster feedback, and if I decide to make a more structured thing like a blog post or ebook later, I know what to focus on

So with that out of the way, let's get into this thread's pattern: traits.

Why use traits in Livewire?

Livewire is kind of a strange technology, because one one hand it's cleaner than anything else if you're looking at technical complexity, but on the other hand the code can get a bit messy and complicated (but in a different way). Basically, you write less code and have to deal with fewer abstractions, but the code itself can get tricky.

One of the reasons is that components often deal with a lot of logic, because there's no good way (currently) to split logic into multiple components without having to use approaches like listeners which trigger separate requests for the other components.

Another reason is that aside from the business logic itself, the components often deal with the frontend presentation too. You can see an example in Jetstream, the backend code sets a property which controls whether the modal is being displayed.

And another, more obvious reason is that you simply may want to reuse logic in multiple components. For example on StackJobs I have multiple components which contain the same UI for adding skills. The developers can add skills to their profiles and the recruiters can add (required/wanted) skills to their job postings.

Abstracting presentation-related logic

Let's start with a very simple example. You want Alpine to see if the component's data is valid. For instance on this forum, the reply dialog only disappears if the reply is processed by the backend and valid, i.e. only if there are no errors that should be shown to the user.

The code of the method looks like this:

public function reply()
{
    if (! auth()->check()) {
        return redirect()->route('forum.login');
    }

    $this->validate([
        'content' => ['required', 'max:4096'],
    ]);

    $this->reply->directChildren()->create([...]);

    // ...
    

The validate() method aborts further execution, so we need to use the dehydrate() hook. To do that, we'd create a method like this:

public function dehydrate()
{
    $this->valid = ! $this->errorBag()->any();
}

Of course, we also need to add the valid property itself:

public bool $valid = true;

Now we can work with $wire.valid in Alpine, and it all works as expected.

However, there are some issues. Or not yet, but likely soon. Specifically:

  • We'll likely use the same logic in multiple components
  • If the component gets more presentation-related logic like this, it will get noisy

To solve that, we can create a trait:

trait WithValidProperty
{
    public bool $valid = true;

    public function dehydrateWithValidProperty()
    {
        $this->valid = ! $this->errorBag->any();
    }
}

This trait is beautiful. It's very short, it will clean up many components, and it houses both the property and the logic. So any components that use this logic will simply add the trait and nothing else. They won't even have to add the property.

class Reply extends Component
{
    use WithValidProperty;

    public string $content = '';

    // ...
}

Abstracting business logic

Above, I mentioned that on StackJobs we use traits to reuse logic between components. To keep this short I won't share the code here, but the traits provide:

  • properties
  • methods
  • data set in mount()

In Lean I also use many traits with computed properties.

Basically, whenever you have logic that's repeated in multiple components, you can extract it into a trait and have the components use the trait instead. Sometimes the traits won't be pure — you'll need abstract methods, code added to the component's mount() for the traits to work, and things like that. But that's okay, because overall the benefit of moving the logic to the trait is much greater.

So these are some examples of how you can use traits to improve your Livewire code. I'll post another thread which shows how I'm using the valid property from the trait above.

Let me know what you think about these threads in the replies!

  • By @ralphjsmit · 2021-09-01 05:54

    Hey Samuel, this is brilliant. What you’re talking about is precisely what I’m also dealing with: components are cluttered very quickly. I love these threads and tips, so thanks!