In the past, when building Livewire components for our Laravel applications, we needed to keep our backend and frontend code split up into separate files. This can sometimes get a little confusing, especially with larger projects. But with Volt, we can now build single-file Livewire components where the backend and frontend code coexist in the same file.

In this article, we'll look at what Volt is and how Volt components differ from traditional Livewire components. We'll then learn how to write tests for your Laravel Volt components.

By the end of the article, you should understand what Volt is and feel confident using it in your Laravel projects.

What is Laravel Volt?

Volt is a fantastic package in the Laravel ecosystem that essentially adds a layer over Livewire so it supports single-file components.

Traditionally, with Livewire, you would have Blade templates that contains the frontend and user interface (UI) and a separate PHP file for the backend logic (such as interacting with the database or service classes and managing state). With Volt, you can create Livewire components where the frontend and backend code coexist in the same file. As a result, you can build isolated and focused single-file components without jumping between two separate files.

If you've used Vue to create user interfaces, you might already be familiar with putting your UI and logic in the same file.

Laravel Volt components come in two different variations: functional and class-based. We'll look at both of these in this article so you can decide which one fits your workflow and preferences.

A traditional Livewire component

To understand the differences between traditional Livewire components and Volt components, we'll first look at what a standard Livewire component looks like in a Laravel web application.

We'll have a simple component that lists users and allows you to select users to delete using checkboxes. We'll then have a button that will delete the selected users. We'll also display a message with the number of selected users.

It's worth noting that this example is purely for demonstration purposes, so don't worry too much about the actual functionality. We won't be including things such as error handling, validation, authorization, or pagination. You'd likely want to include these in your production components.

We'll start by looking at the Livewire component's backend logic and then discuss what it's doing:

declare(strict_types=1);

namespace App\Livewire\Users;

use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Locked;
use Livewire\Component;

final class Index extends Component
{
    /** @var Collection<int, User> */
    #[Locked]
    public Collection $users;

    /** @var int[] */
    public array $selectedUserIds = [];

    public function mount(): void
    {
        $this->users = User::all();
    }

    public function render(): View
    {
        return view('livewire.users.index');
    }

    /**
     * Define a public function that can be called from the
     * frontend and the backend.
     */
    public function deleteSelected(): void
    {
        User::query()->whereKey($this->selectedUserIds)->delete();

        $this->reloadPage();
    }

    /**
     * Define a function that can only be called from the backend.
     */
    protected function reloadPage(): void
    {
        $this->redirect(route('users.index'));
    }
}

In the code above, we can see that we have an App\Livewire\Users\Index class that extends the Livewire\Component class.

Because the users property is public, the frontend can access it by default—which we don't want. Adding the Livewire\Attributes\Locked attribute means the property can only be updated from the backend (within this PHP class).

The class also contains a selectedUserIds property (an array of integers), which it uses to store the IDs of the selected users on the frontend.

The class contains the following methods:

  • mount - Called when the component is first mounted and loaded on the page. In this method, we're fetching all the users from the database and storing them in the users property.
  • render - Renders the view, which is located in resources/views/livewire/users/index.blade.php.
  • deleteSelected - Called when the user clicks the "Delete Selected" button. It will delete all the users that were selected on the frontend. After deleting the users, the reloadPage method reloads the page. The deleteSelected method is public and can be called from both the front and backend.
  • reloadPage - Reloads the page after the users are deleted. I've created this method purely for demonstration purposes so that we can look at protected methods. Since it's protected, the method can only be called from the backend, not the frontend.

Now that we have the backend logic for our Livewire component, let's take a look at the frontend/UI Blade file (located at resources/views/livewire/users/index.blade.php as defined in our component's render method):

<div>
    Users selected: {{ count($selectedUserIds) }}

    @if(count($selectedUserIds))
         <button wire:click="deleteSelected">
             Delete Selected
         </button>
    @endif

    @foreach($users as $user)
        <div>
            <input
                type="checkbox"
                wire:model.live="selectedUserIds"
                value="{{ $user->id }}"
            >
            {{ $user->name }}
        </div>
    @endforeach
</div>

In the simple example above, we have a Blade file that contains the HTML for the component. First, we display the number of users that the user selected using the checkboxes. For example, if the user has selected three users, it will output "Users selected: 3".

We're then using an @if directive to check if any users are selected. If there are selected users, we'll show a button that the user can click to delete the selected users. This button contains the wire:click directive to call the deleteSelected method on the backend when clicked.

Following this, we're looping through all the users and displaying a checkbox for each user. The wire:model.live directive binds the checkbox to the selectedUserIds property; when a user checks a checkbox, the component adds the ID of the user to the selectedUserIds array. When a user unchecks a checkbox, the component removes the user's ID from the selectedUserIds array. It's worth noting that I've used wire:model.live instead of wire:model as we want the changes to be reflected immediately on the frontend and cause Livewire to re-render the component. You probably wouldn't want to do this in a real-life Laravel project as it could cause unnecessary Livewire requests to the server; instead, you'd opt for using something like Alpine JS to handle the checkbox changes via JavaScript to prevent needing to make any requests to the server to re-render the page. But since we're focusing purely on Livewire and Volt in this example, we're using wire:model.live.

Finally, to render the component in a Blade file, you could do something like this:

<html>
<head>
    <title>Users</title>
</head>
<body>
    ...

    <livewire:users.index />

    ...
</body>
</html>

As we can see in the Blade file above, we're using <livewire:users.index> to render the Livewire component that exists in the App\Livewire\Users\Index class.

That covers all of our simple Livewire component. Now, let's look at how we can achieve the same functionality using a Laravel Volt component.

Installing Volt

To get started with using Volt, you'll first need to download it using Composer by running the following command:

composer require livewire/volt

You can then run Volt's installation command by running the following Artisan command in your terminal:

php artisan volt:install

Volt is now installed and ready for you to use.

Class-based Volt components

First, we'll look at a class-based Laravel Volt component, which is the most similar to traditional Livewire components. The component's functionality will be the same as the Livewire component we looked at earlier.

To create the component, we can run the following command:

php artisan make:volt users/index --class

This will create a new resources/views/livewire/users/index.blade.php file that looks like this:

<?php

use Livewire\Volt\Component;

new class extends Component {
    //
}; ?>

<div>
    //
</div>

In the component outline above, we can see that there are two separate sections. The first section is the PHP section (between the <?php and ?> tags), and the second section is the HTML section (between the <div> and </div> elements). The PHP is where our backend logic will go (similar to the Livewire component), and the HTML is where our frontend/UI will go.

Let's update our class-based Laravel Volt component to match the functionality of the Livewire component we looked at earlier:

<?php

use App\Models\User;
use Illuminate\Contracts\View\View;
use Illuminate\Database\Eloquent\Collection;
use Livewire\Attributes\Locked;
use Livewire\Volt\Component;

new class extends Component {
    /** @var Collection<int, User> */
    #[Locked]
    public Collection $users;

    public array $selectedUserIds = [];

    public function mount(): void
    {
        $this->users = User::all();
    }

    /**
     * Define a public function that can be called from the
     * frontend and the backend.
     */
    public function deleteSelected(): void
    {
        User::query()->whereKey($this->selectedUserIds)->delete();

        $this->reloadPage();
    }

    /**
     * Define a function that can only be called from the backend.
     */
    protected function reloadPage(): void
    {
        $this->redirect(route('users.index'));
    }
};

?>

<div>
    Users selected: {{ count($selectedUserIds) }}

    @if(count($selectedUserIds))
        <button wire:click="deleteSelected">
            Delete Selected
        </button>
    @endif

    @foreach($users as $user)
        <div>
            <input
                type="checkbox"
                wire:model.live="selectedUserIds"
                value="{{ $user->id }}"
            >
            {{ $user->name }}
        </div>
    @endforeach
</div>

In the component above, we can see that the component's PHP logic (in the PHP section) is very similar to that of the Livewire component. The only differences are:

  • We're creating an anonymous class rather than a named class.
  • The anonymous class is extending the Livewire\Volt\Component class instead of the Livewire\Component class.
  • We don't have the render method because the Blade is included in the same file.

So, apart from those minor differences, this Volt component almost looks like a typical Livewire component but with the backend logic and frontend/UI in the same file. Pretty cool, right?

Functional Volt components

Next, let's look at how we could define the component using the functional approach for our web application. This approach is essentially an elegantly crafted functional API for Livewire.

To create a functional Volt component, you can run the following command:

php artisan make:volt users/index --functional

This will create a new resources/views/livewire/users/index.blade.php file that looks like this:

<?php

use function Livewire\Volt\{state};

//

?>

<div>
    //
</div>

As we can see, the component outline is similar to the class-based Volt component, but it doesn't have a class definition. We're also importing a function from the Livewire\Volt namespace called state that we'll be using to define properties.

Let's update this component and then discuss what's happening:

<?php

use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use function Livewire\Volt\protect;
use function Livewire\Volt\state;

// Define a property that can be updated from both
// the frontend and the backend.
state([
    'selectedUserIds' => [],
]);

// Define a property that can only be updated on the backend.
state([
    'users' => fn () => User::all(),
])->locked();

// Define a public function that can be called from the
// frontend and the backend.
$deleteSelected = function (): void {
    User::query()->whereKey($this->selectedUserIds)->delete();

    $this->reloadPage();
};

// Define a function that can only be called from the backend.
$reloadPage = protect(function (): void {
    $this->redirect(route('users.index'));
});

?>

<div>
    Users selected: {{ count($selectedUserIds) }}

    @if(count($selectedUserIds))
        <button wire:click="deleteSelected">
            Delete Selected
        </button>
    @endif

    @foreach($users as $user)
        <div>
            <input
                type="checkbox"
                wire:model.live="selectedUserIds"
                value="{{ $user->id }}"
            >
            {{ $user->name }}
        </div>
    @endforeach
</div>

In the code above, we can see that the frontend/UI code in the <div> tags is the same as the class-based Volt component and the Livewire component we looked at earlier. The main differences are in the PHP section.

We're first starting by using Volt's state function to define the selectedUserIds and users properties. We could declare both of these in the same state function call, but I've split them out since we want to lock the users' property to prevent the frontend from updating it by chaining the locked method onto the state function call. This is the same as using the Livewire\Attributes\Locked attribute in our previous examples.

Following this, we're defining our two methods: deleteSelected and reloadPage. This may be slightly confusing if you're coming from the class-based approach because we define them as variables. But by creating a deleteSelected variable and assigning it a function, we're essentially creating a public method.

You may have noticed that we've wrapped the reloadPage function in the protect function. This prevents the method from being called from the frontend, similar to defining a protected method in a class-based component.

As we can see, the functional approach is quite different from the class-based approach. However, you may feel more comfortable with this approach—especially if you're used to working with something like Vue components in this way.

Testing Volt components

Like any other part of your Laravel application, it's important to write tests for your Volt components to ensure they work as expected.

One of the great features of using Volt and Livewire is that they provide testing helpers that make it easy for you to do this. Apart from a few minor differences, you can test your Volt components like you'd typically test your Livewire components. If you're unfamiliar with testing in Livewire, you may want to check out the testing section in the documentation.

Lastly, let's look at how you can write tests for your Volt components.

Testing the component is in the view

You'll likely want to test whether a Volt component is actually in your Blade views to display. To do this, you can use the assertSeeVolt method that Volt provides.

For example, say you have a route with the name of admin.users.index, which returns a resources/views/users/index.blade.php Blade file that renders a resources/views/livewire/users/list.blade.php Volt component. You could write a test like so:

declare(strict_types=1);

namespace Tests\Feature;

use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

final class RoutesTest extends TestCase
{
    use LazilyRefreshDatabase;

    #[Test]
    public function volt_component_is_in_the_view(): void
    {
        $this->get(route('admin.users.index'))
            ->assertOk()
            ->assertViewIs('users.index')
            ->assertSeeVolt('users.list');
    }
}

With a test like this, you can have confidence that the users.index Blade view renders the users.list Volt component as expected.

Testing the component's logic

You can also test the functionality of the Volt component itself. Thankfully, no matter if you choose the functional or class-based approach, you can test the components in the same way.

In our example above, we had a simple component that allowed users to select users to delete and then delete them. So, we might want to test the following things:

  • The component can be rendered and includes the users.
  • The selected users can be deleted.
  • An error is thrown if the user tries to update the locked users property.
  • An error is thrown if the user tries to call the protected reloadPage method.

Of course, in a real-life scenario, we'd also want to test scenarios such as:

  • An error is thrown if we press the "Delete Selected" button and no users are selected.
  • An error is thrown if we don't have permission to delete users.
  • An error is thrown if the user tries to delete a user that doesn't exist.

But for this article, we're keeping things simple.

Let's take a look at our tests and then discuss what's happening:

declare(strict_types=1);

namespace Tests\Feature\Livewire\Users;

use App\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Livewire\Exceptions\MethodNotFoundException;
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
use Livewire\Volt\Volt;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

final class IndexTest extends TestCase
{
    use LazilyRefreshDatabase;

    /** @var Collection<int,User>  */
    private Collection $users;

    protected function setUp(): void
    {
        parent::setUp();

        $this->users = User::factory()
            ->count(5)
            ->create();
    }

    #[Test]
    public function livewire_component_can_be_rendered(): void
    {
        Volt::test('users.index')
            ->assertOk()
            ->assertViewHas('users');
    }

    #[Test]
    public function users_can_be_deleted(): void
    {
        Volt::test('users.index')
            ->set('selectedUserIds', [
                $this->users->first()->id,
                $this->users->last()->id,
            ])
            ->call('deleteSelected')
            ->assertRedirect(route('admin.users.index'));

        $this->assertDatabaseCount('users', 3);
        $this->assertModelMissing($this->users->first());
        $this->assertModelMissing($this->users->last());
    }

    #[Test]
    public function error_is_thrown_if_the_user_tries_to_update_the_users_property(): void
    {
        $this->expectException(CannotUpdateLockedPropertyException::class);

        Volt::test('users.index')
            ->set('users', 'dummy value');
    }

    #[Test]
    public function error_is_thrown_if_trying_to_call_the_reloadPage_protected_method(): void
    {
        $this->expectException(MethodNotFoundException::class);

        Volt::test('users.index')
            ->call('reloadPage');
    }
}

You may have noticed in the tests above that we're using the Livewire\Volt\Volt class to test our Volt components. Livewire provides this class and allows you to test your Volt components similarly to how you'd test your Livewire components. We start our assertions by calling the test method on the Volt class and passing in the name of the Volt component we want to test.

In the first test (livewire_component_can_be_rendered), we're testing that the Volt component can be rendered and includes the users. We're doing this by using the assertViewHas method to check that the users property is in the view.

The second test (users_can_be_deleted) verifies that users can be deleted. We're doing this by setting the selectedUserIds property using the set method (as if the user had just selected the checkboxes in the UI). We're then calling the deleteSelected method (as if the user had clicked the "Delete Selected" button) and asserting that the users have been deleted from the database. We also check that the user is redirected to the correct route.

In the third test (error_is_thrown_if_the_user_tries_to_update_the_users_property), we're testing that an error is thrown if the user tries to update the locked users property. We're doing this by attempting to set the users property to a dummy value and then asserting that a Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException exception is thrown. This can give us confidence that we're not accidentally allowing any properties to be updated from the frontend when they shouldn't be.

Finally, in the fourth test, we're testing that an error is thrown if the user tries to call the protected reloadPage method. We're doing this by attempting to call the reloadPage method and asserting that a Livewire\Exceptions\MethodNotFoundException exception is thrown. By doing this, we can be confident that we're not exposing any methods that the user shouldn't be able to call.

Should I use Volt?

Now that we've discussed what Volt is and how to use it, you may ask yourself, "Should I use Volt or traditional Livewire?" and "Should I use functional or class-based Volt components?"

The answer to these questions is mainly a personal preference and what works best for you and your team.

One of the things I've found most helpful about Volt is that I can quickly build focused, isolated components where all the logic and view code are in the same file. I've also found this slightly easier when revisiting an existing component, as I don't have to navigate between two separate files to understand what's happening.

However, I've heard from some developers who are part of teams with dedicated frontend and backend developers that using Volt has caused some difficulties for them. They found a higher chance of conflicts in Git due to the lack of separation between the UI and logic. So, they may have had a frontend developer working on the UI and a backend developer working on the same file simultaneously—something to consider if you're working in a team where you might be working on components together. However, in an ideal world, I'd like to think this can be avoided (or at least reduced) with good communication within the team.

The choice between the functional API and class-based Laravel Volt components is also a personal preference. You might feel more comfortable with the functional approach if you come from a Vue background. However, if you're like me and are used to working with traditional Livewire components, you'll likely feel more comfortable with the class-based approach. But if you're unsure, I'd recommend trying both approaches and seeing which one you prefer. The main thing is to choose a single approach you and your team feel comfortable with and stick to it to keep your codebase consistent.

Personally, I really enjoyed working with Volt, and I plan to use the class-based approach in future Laravel projects.

Conclusion

In this article, we looked at what Laravel Volt is and how to write tests for your Volt components. We also looked at the differences between traditional Livewire components and Volt components.

Hopefully, you should now feel confident enough to start using Volt in your Laravel projects.

Will you be using Volt in your new project?

To stay up to date with the latest Laravel and PHP articles, sign up for the Honeybadger newsletter.

author photo
Ashley Allen

Ashley is a freelance Laravel web developer who loves contributing to open-source projects and building exciting systems to help businesses succeed.

More articles by Ashley Allen