Confession: I avoided Laravel Volt for almost a year. Single-file Livewire components sounded like the kind of thing Twitter gets excited about for two weeks and then nobody mentions again. I figured I’d wait it out. Six months ago I caved and rewrote a small admin panel with it. Now most new Livewire work in my Laravel projects starts as a Volt file, and the older class-based components are slowly migrating over.
This isn’t a “Volt is the future, here’s why you must rewrite everything” post. It’s a list of where Volt actually pays off, where it doesn’t, and the bits that bit me. If you’re on the fence, this is the one I wish I’d read.
What Laravel Volt actually is
If you haven’t seen it, Laravel Volt is a thin functional API on top of Livewire 3. Instead of one PHP class plus one Blade file per component, you write a single .blade.php file with the component logic, state, and template all in the same place. It’s still Livewire underneath. The wire model, the morphing, the network round-trips — all of it. Volt only changes how you author the component.
The reason it works is that PHP got better. Closures and the new property hooks in PHP 8.4 made it possible to express component state without a class declaration. I wrote about this shift in how PHP 8.4 property hooks changed how I write classes, and Volt is one of the cleanest places that change shows up.
Here’s the smallest possible counter example. A class-based Livewire counter looks like this:
<?php
namespace App\Livewire;
use Livewire\Component;
class Counter extends Component
{
public int $count = 0;
public function increment(): void
{
$this->count++;
}
public function render()
{
return view('livewire.counter');
}
}
Plus a separate resources/views/livewire/counter.blade.php. The same thing in Volt:
<?php
use function Livewire\Volt\{state};
state(['count' => 0]);
$increment = fn () => $this->count++;
?>
<div>
<button wire:click="increment">+</button>
<span>{{ $count }}</span>
</div>
One file. No namespace. No Component parent class. The Livewire runtime is identical. That’s the whole pitch.
Where Volt earned a spot in my workflow
The honest answer is “anywhere a component is small enough that the class file felt like ceremony.” For me, that turned out to be most of them.
The clearest win is dashboard widgets. I have a Filament-adjacent admin panel where each card is its own Livewire component — a stat card, a “recent signups” list, a tiny line chart fed by a Postgres query. In the class version, I had app/Livewire/Dashboard/RevenueCard.php and resources/views/livewire/dashboard/revenue-card.blade.php, and any time I needed to tweak the markup I’d jump between two folders. With Volt, every card is one file in resources/views/livewire/dashboard/. Reviewing a PR that touches three cards is genuinely faster.
The other win is forms. A signup form, a contact form, a “rename project” modal — these are typically thirty lines of state and a couple of validation rules. Spreading that across two files always felt heavier than it needed to be. Volt collapses it without losing any of the Livewire features I actually use, like real-time validation with wire:model.live and the #[Validate] attribute.
Where I still reach for class-based components: anything I’d want to test in isolation, anything with multiple authorization paths, and anything where the public methods are a real API consumed by other components. Tests are the big one. You can test Volt components, but the ergonomics around Livewire::test() are still cleaner with a class you can import. I’ve also found that components which dispatch a lot of events benefit from a class — the method names show up in the IDE’s outline view, which matters once you have eight or ten of them.
The Folio + Volt thing is bigger than people say
The combination most write-ups undersell is Laravel Folio plus Volt. Folio gives you file-based routing — drop resources/views/pages/dashboard.blade.php and it becomes the /dashboard route. Add Volt to that file and you have a full Livewire-backed page in a single file with no controller, no route definition, and no separate view.
For internal tools and admin panels I’ve stopped writing controllers entirely. Last month I built a small migration runner UI for a client — a page that lists pending data migrations, lets the operations team approve them, and shows a tail of the job log. The whole thing is one file:
<?php
use function Livewire\Volt\{state, computed};
use App\Models\Migration;
state(['filter' => 'pending']);
$migrations = computed(fn () =>
Migration::where('status', $this->filter)
->latest()
->limit(50)
->get()
);
$approve = function (int $id) {
Migration::findOrFail($id)->approve();
};
?>
<x-layouts.admin>
<select wire:model.live="filter">
<option value="pending">Pending</option>
<option value="approved">Approved</option>
<option value="failed">Failed</option>
</select>
@foreach ($this->migrations as $m)
<div>
<strong>{{ $m->name }}</strong>
<button wire:click="approve({{ $m->id }})">Approve</button>
</div>
@endforeach
</x-layouts.admin>
No web.php entry. No MigrationController. No separate Livewire class. The routing, the state, the queries, and the markup all live together. For internal tools that maybe ten people will ever use, this is the right amount of structure. The Laravel docs cover the Folio basics but spend most of their time on routing — try it with Volt before you decide whether file-based pages are for you.
If you’re coming from Next.js, the mental model is similar to a server action inside an app/ route handler. The big difference is that Livewire keeps a stateful component on the server between interactions, so you don’t have to re-derive everything on each request. That’s a real architectural difference, and it’s the thing I’d push back on when people say Volt is “Laravel’s answer to React.” It’s not. It’s Laravel’s answer to “I want a stateful UI without leaving PHP.”
The parts that bit me
Volt isn’t a free win. Three things tripped me up in the first weeks.
The first is that closures don’t auto-bind $this to the component the way class methods do — except when they kind of do, because Volt rewires them to the component instance. Most of the time it just works. But if you nest a closure inside another closure, or pass a closure to a collection method, the $this reference can become whatever PHP decides it should be at runtime. The fix is usually use ($this) or extracting to a named function, but the error messages aren’t always clear. I lost an afternoon on this once and now I default to named arrow functions for anything more than a one-liner.
The second is editor support. PhpStorm’s Livewire plugin works well with class-based components — go-to-definition for Blade <livewire:foo> tags, autocompletion of public properties, the works. With Volt, some of that still hasn’t caught up. The state() function is dynamic enough that static analysis doesn’t always know what properties exist on the component. If you live in PhpStorm, this is real friction. VS Code with the Laravel extension is, oddly, a better Volt experience right now.
The third is that Volt files can grow. The single-file ergonomics that feel great at sixty lines start to feel cramped at three hundred. There’s no hard line, but my rough rule is: if I scroll past a full screen of PHP before hitting the closing ?>, I split the data layer into a service or an Action class and keep Volt as the thin presentation layer. The same rule I’d apply to a React component, basically.
How I’d ramp up on Volt today
If you’re starting fresh, I’d skip the class-based Livewire tutorials and go straight to Volt. The official Livewire quickstart shows the class version, but the Volt section is short enough to read in fifteen minutes and you’ll see the whole shape of the API.
If you’re already on Livewire 3, don’t migrate. New components in Volt, old ones stay where they are. Mixed codebases work fine — Livewire doesn’t care which authoring style produced a component. I cover this kind of incremental migration approach in how I think about adopting framework features when working with client codebases, and Volt is a textbook case for it.
The one thing I’d avoid is treating Volt as “Livewire but easier.” It’s a different mental model — closer to React function components than to PHP classes. The first week I kept reaching for $this->dispatch() patterns that worked in class components and confused myself. Spend a day reading the Volt source if you want to understand what’s happening. It’s only a few hundred lines.
What to try this week
Pick one Livewire component in your project that’s under fifty lines of PHP and convert it to Volt. Don’t touch the Blade. Just collapse the class into the view file using state() and arrow functions. If it feels lighter, do another one. If it feels worse, you’ve spent an hour and learned where Volt isn’t for you.
That’s how I ended up here. One file at a time, no big rewrites, no team-wide decree. Volt earned its place by being slightly less work for a specific kind of component, and after six months that’s still the version of the pitch I’d give.