Livewire 3 vs 4 — what changed

Everything that changed between Livewire 3 and Livewire 4: the new features worth adopting, every breaking change with before-and-after code, and a checklist for the upgrade itself. Including the three changes that pass your test suite and break production.

Livewire 3 → 4 Breaking changes Migration checklist 14 sections

1. The short version

Livewire 4 is a big release with a small upgrade path. The component model did not change: PHP class, Blade view, state on the server, HTML over the wire. What changed is where components live, how they are written, and how much of the page a single update touches.

Class-based components keep working. Single-file is the default for new components, not a forced rewrite. Most Livewire 3 applications upgrade by bumping the version, fixing a handful of breaking changes, and adopting the new features gradually.

What you gain

  • Single-file components — class and markup in one file.
  • Islands — regions that re-render independently, without splitting into child components.
  • Slots — parents inject markup into children, evaluated in the parent's context.
  • Optimistic UIwire:show, wire:text, wire:bind update the DOM with no round trip.
  • Drag and dropwire:sort, no external library.
  • Scoped CSS — component styles that cannot leak.

What will bite you

  1. Unclosed <livewire:…> tags silently render nothing.
  2. wire:model modifiers changed meaning — fields stop syncing.
  3. The update endpoint moved to /livewire-{hash}/ — infrastructure rules break.
  4. wire:transition modifiers were removed.
  5. Volt folded into core.

Each of these is covered in detail below, with before-and-after code.

2. Side by side

AreaLivewire 3Livewire 4
Component files app/Livewire/Counter.php + resources/views/livewire/counter.blade.php resources/views/components/⚡counter.blade.php (single file)
Make command php artisan livewire:make Counter php artisan make:livewire counter
Routing Route::get('/x', Dashboard::class) Route::livewire('/x', 'pages::dashboard')
Partial rendering Split into child components @island regions
Slots Not supported {{ $slot }}, <wire:slot name="…">
Drag and drop SortableJS plus glue code wire:sort
Instant UI Alpine.js by hand wire:show, wire:text, wire:bind
Component CSS Global stylesheet Scoped <style> in the component
Parallel actions Not available #[Async], wire:click.async
Update endpoint /livewire/update /livewire-{hash}/update
JS hooks Livewire.hook('request'|'commit') interceptRequest(), interceptMessage()
Volt Separate package Folded into core
Testing PHPUnit or Pest, tests/Feature/Livewire Pest recommended, tests can sit beside the component

3. The upgrade, step by step

Bash
# 1. Bump the dependency
composer require livewire/livewire:^4.0

# 2. Clear every cache — stale compiled views are the usual first failure
php artisan optimize:clear

# 3. Run your test suite before touching anything else
php artisan test

Laravel Shift can automate a large part of the mechanical work. Whether you use it or not, work through the checklist below afterwards — the silent failures are the ones a tool is least likely to flag.

Order of work that tends to go smoothly

  1. Upgrade the package and clear caches.
  2. Fix the config renames (chapter 4).
  3. Fix routing (chapter 5).
  4. Search the codebase for unclosed component tags (chapter 6).
  5. Audit every wire:model with a modifier (chapter 7).
  6. Update infrastructure rules for the new endpoint (chapter 8).
  7. Replace removed wire:transition modifiers (chapter 9).
  8. Migrate Volt components if you use Volt (chapter 12).
  9. Run the suite again, then click through the app in a browser.
  10. Only then start adopting islands, slots and single-file components.
Do not combine the upgrade with a rewrite. Get the app green on v4 with your existing class-based components first. Converting to single-file components and islands is a separate piece of work with its own review.

4. Configuration renames

Livewire 3Livewire 4Note
layoutcomponent_layoutNow uses the layouts:: namespace
lazy_placeholdercomponent_placeholder
smart_wire_keyssmart_wire_keysDefault flipped from false to true
component_locationsDirectories scanned for components
component_namespacesNamed roots such as pages::
make_commandSingle-file or class-based generation
csp_safeBuild compatible with stricter CSP
Livewire 3 — config/livewire.php
'layout' => 'layouts.app',

'lazy_placeholder' => 'livewire.placeholder',
Livewire 4 — config/livewire.php
'component_layout' => 'layouts::app',

'component_placeholder' => 'livewire.placeholder',

'component_locations' => [
    'resources/views/components',
    'resources/views/livewire',
],

'component_namespaces' => [
    'pages' => 'resources/views/pages',
],

// Keep generating class-based components
'make_command' => [
    'type' => 'class',
],

5. Routing

Full-page components get a dedicated route macro.

Livewire 3
use App\Livewire\Dashboard;

Route::get('/dashboard', Dashboard::class)
    ->middleware('auth')
    ->name('dashboard');
Livewire 4
use App\Livewire\Dashboard;

// By class
Route::livewire('/dashboard', Dashboard::class)
    ->middleware('auth')
    ->name('dashboard');

// Or by component name, for view-based components
Route::livewire('/dashboard', 'pages::dashboard')
    ->middleware('auth')
    ->name('dashboard');
A quick way to find them all: grep -rn "Route::get(.*::class" routes/, then check which of those classes extend Livewire\Component.

6. Component tags must be closed

The nastiest one, because it fails silently. An unclosed component tag renders nothing at all — no exception, no log entry, just a missing block on the page.
Livewire 3 — tolerated
<livewire:user-profile>
<livewire:order-list>
Livewire 4 — required
<livewire:user-profile />
<livewire:order-list />

Finding them

Bash
# Component tags that are not self-closed
grep -rn "<livewire:[^>]*[^/]>" resources/views/

# The @livewire() directive form is unaffected
grep -rn "@livewire(" resources/views/

Once the app is running, a click-through of the main flows is worth the time: a missing block is easy to miss in a diff and obvious on screen.

7. wire:model changed twice

Two independent changes, both of which produce a field that "just stops working" rather than an error. Audit every wire:model that carries a modifier.

7.1 Modifiers now control client-side syncing

In v3, .blur and .change decided when a network request fired. The value itself was always tracked on the client. In v4 they decide when the value syncs at all. Add .live to restore the old behaviour.

Livewire 3
<input wire:model.blur="title">
<select wire:model.change="status">
Livewire 4 — same behaviour as before
<input wire:model.live.blur="title">
<select wire:model.live.change="status">

7.2 No more bubbling from child elements

wire:model on a wrapper no longer captures events that bubble up from nested inputs.

Livewire 3 — the wrapper caught it
<div wire:model="value">
    <input type="text">
</div>
Livewire 4 — opt in explicitly
<div wire:model.deep="value">
    <input type="text">
</div>

Finding them

Bash
# Every wire:model with a modifier
grep -rn "wire:model\.[a-z]" resources/views/

# wire:model on a non-input element is a bubbling candidate
grep -rn "<div[^>]*wire:model" resources/views/
Custom form components — a date picker, a rich text editor, a wrapped select — are the most likely victims of the bubbling change, because the wrapper pattern is exactly how they are usually built.

8. The update endpoint moved

All Livewire URLs now include a hash: /livewire/ became /livewire-{hash}/.

This one passes every test and breaks production. Nothing in your PHP code references the path, so the suite stays green. The failure appears only where infrastructure has a literal /livewire/ rule.

Places to check

  • nginx or Apache location blocks
  • WAF and firewall allow-lists
  • CDN cache-bypass rules (Cloudflare page rules and similar)
  • Rate limiters keyed on the path
  • Load balancer routing rules
  • Monitoring and uptime checks pointed at the endpoint
  • CSP connect-src entries pinned to a path
nginx — before
location /livewire/ {
    # …
}
nginx — after
location ~ ^/livewire(-[a-z0-9]+)?/ {
    # …
}

9. wire:transition was rebuilt

wire:transition no longer accepts modifiers such as .opacity, .scale or .duration.200ms. It is built on the native View Transitions API now, and the animation is expressed in CSS.

Livewire 3
<div wire:transition.opacity.duration.300ms>
    …
</div>
Livewire 4
<div wire:transition>
    …
</div>
CSS
::view-transition-old(root),
::view-transition-new(root) {
    animation-duration: 300ms;
}
Browser support for View Transitions is not universal. Where it is missing the content simply appears without an animation — a graceful degradation, but worth knowing before you rely on the effect for clarity rather than polish.

10. PHP API changes

Streaming

Livewire 3
$this->stream(to: '#container', content: 'Hello', replace: true);
Livewire 4
$this->stream('Hello', replace: true, el: '#container');

Component mounting

Livewire 3
mount($name, $params = [], $key = null)
Livewire 4
mount($name, $params = [], $key = null, $slots = [])

Relevant if you mount components programmatically or have custom code that wraps Livewire's mounting. Ordinary application code is unaffected.

New attributes worth knowing

AttributePurpose
#[Async]Run an action in parallel, outside the request queue
#[Renderless]Skip the re-render after an action
#[Prop]Declare a property as a parent-supplied prop
#[Json]Return data straight to JavaScript

11. JavaScript API changes

$js actions

Livewire 3
$wire.$js('showToast', { message: 'Saved' });
Livewire 4
$wire.$js.showToast = { message: 'Saved' };

Hooks became interceptors

The commit and request hooks are deprecated in favour of interceptMessage() and interceptRequest().

Livewire 3
document.addEventListener('livewire:init', () => {
    Livewire.hook('request', ({ options, fail }) => {
        options.headers['X-Tenant'] = window.tenantId;

        fail(({ status, preventDefault }) => {
            if (status === 419) {
                preventDefault();
                window.location.reload();
            }
        });
    });
});
Livewire 4
document.addEventListener('livewire:init', () => {
    Livewire.interceptRequest(({ options, fail }) => {
        options.headers['X-Tenant'] = window.tenantId;

        fail(({ status, preventDefault }) => {
            if (status === 419) {
                preventDefault();
                window.location.reload();
            }
        });
    });

    Livewire.interceptMessage(({ component, succeed }) => {
        succeed(() => {
            console.debug('component updated', component.name);
        });
    });
});

New magic properties

Blade
{{-- Validation errors, on the client --}}
<div x-show="$errors.has('email')" x-text="$errors.first('email')"></div>

12. Volt folded into core

Volt's single-file syntax was the prototype for what became Livewire 4's single-file components. The separate package is no longer needed.

VoltLivewire 4
Livewire\Volt\ComponentLivewire\Component
Volt::route()Route::livewire()
Volt::test()Livewire::test()
Volt service providerRemove it
livewire/volt packageRemove the dependency
Bash
composer remove livewire/volt
Before (Volt)
<?php

use Livewire\Volt\Component;

new class extends Component {
    public string $title = '';
};

?>

<div>…</div>
After (Livewire 4)
<?php

use Livewire\Component;

new class extends Component {
    public string $title = '';
};

?>

<div>…</div>

13. Adopting the new features

Once the app is green on v4, these are the changes that pay for themselves fastest, roughly in order of return on effort.

1. Islands, on your slowest page

Find the page where one small interaction re-runs a lot of queries — usually a dashboard or a table with filters. Wrap the independent regions in @island.

Before — the whole component re-renders
<div>
    <div class="card">Revenue: {{ $this->revenue }}</div>
    <div class="card">Pending jobs: {{ $this->queue }}</div>
    <div class="card">Latest activity: …</div>
</div>
After — each card updates on its own
<div>
    @island(name: 'revenue')
        <div class="card">Revenue: {{ $this->revenue }}</div>
    @endisland

    @island(name: 'queue', poll: '5s')
        <div class="card">Pending jobs: {{ $this->queue }}</div>
    @endisland

    @island(name: 'activity', lazy: true)
        @placeholder
            <div class="card skeleton animate-pulse h-48"></div>
        @endplaceholder

        <div class="card">Latest activity: …</div>
    @endisland
</div>

2. Replace round trips with client directives

Before
<button wire:click="$toggle('showFilters')">Filters</button>

@if ($showFilters)
    <div class="filters">…</div>
@endif
After — no request at all
<button wire:click="$toggle('showFilters')">Filters</button>

<div class="filters" wire:show="showFilters">…</div>

3. Drop SortableJS

After
<ul wire:sort="reorder">
    @foreach ($tasks as $task)
        <li wire:sort:item="{{ $task->id }}" wire:key="task-{{ $task->id }}">
            <span wire:sort:handle>⠿</span> {{ $task->title }}
        </li>
    @endforeach
</ul>

4. Collapse child components that only existed for rendering

In v3 the standard fix for "this re-renders too much" was a child component. If a child has no state of its own and is not reused, an island in the parent is lighter — one component, one class, one snapshot.

5. Use #[Renderless] for invisible work

PHP
use Livewire\Attributes\Renderless;

#[Renderless]
public function trackView(): void
{
    $this->post->increment('views');
}

6. Convert components to single-file — last, and only where it helps

This is cosmetic. Do it opportunistically as you touch files, not as a migration sprint. Small components benefit; components with heavy logic and many dependencies are often clearer as classes.

14. Upgrade checklist

Before you start

  • The test suite passes on Livewire 3.
  • A branch, and a database backup if the environment is shared.
  • Someone available to click through the app afterwards — the silent failures need eyes.

Mechanical changes

  • composer require livewire/livewire:^4.0
  • php artisan optimize:clear
  • layoutcomponent_layout (and the layouts:: namespace)
  • lazy_placeholdercomponent_placeholder
  • Route::get(…, Component::class)Route::livewire(…)
  • Every <livewire:…> tag self-closed
  • wire:model.blur / .change → add .live
  • Wrapper wire:model → add .deep
  • wire:scrollwire:navigate:scroll
  • wire:transition modifiers → CSS
  • $this->stream() argument order
  • $wire.$js('name', …)$wire.$js.name = …
  • Livewire.hook('request'|'commit')interceptRequest() / interceptMessage()
  • Volt classes, routes, tests and package removed

Infrastructure

  • nginx / Apache location blocks widened for /livewire-{hash}/
  • WAF, CDN and firewall rules updated
  • Rate limiters and monitoring checks repointed
  • CSP connect-src reviewed

Verification

  • Test suite green
  • Every form submits and validates
  • File uploads work, including the progress indicator
  • Tables paginate, sort and filter
  • Modals open and close
  • Third-party widgets inside wire:ignore still initialise
  • Browser console clean on the main flows
  • Update requests return 200 in production, not 403 from a WAF
Roll out behind a canary if you can. The two failure modes that survive a green test suite — unclosed tags and the moved endpoint — both show up immediately in real traffic and are trivial to roll back.

Official resources

Livewire 4 is evolving quickly. Where this article and the official upgrade guide disagree, the official guide is authoritative — read it before an upgrade that matters.