Upgrade Guide

Are you a visual learner?
Master Livewire with our in-depth screencasts
Watch now

Automated upgrade tool

To save you time upgrading, we've included an Artisan command to automate as many parts of the upgrade process as possible.

After installing Livewire version 3, run the following command, and you will receive prompts to upgrade each breaking change automatically:

php artisan livewire:upgrade

Although the above command can upgrade much of your application, the only way to ensure a complete upgrade is to follow the step-by-step guide on this page.

Hire us to upgrade your app instead

If you have a large Livewire application or just don't want to deal with upgrading from version 2 to version 3, you can hire us to handle it for you. Learn more about our upgrade service here.

Upgrade PHP

Livewire now requires that your application is running on PHP version 8.1 or greater.

Update Livewire to version 3

Run the following composer command to upgrade your application's Livewire dependency from version 2 to 3:

composer require livewire/livewire "^3.0"
Livewire 3 package compatibility

Most of the major third-party Livewire packages either currently support Livewire 3 or are working on supporting it soon. However, there will inevitably be packages that take longer to release support for Livewire 3.

Clear the view cache

Run the following Artisan command from your application's root directory to clear any cached/compiled Blade views and force Livewire to re-compile them to be Livewire 3 compatible:

php artisan view:clear

Merge new configuration

Livewire 3 has changed multiple configuration options. If your application has a published configuration file (config/livewire.php), you will need to update it to account for the following changes.

New configuration

The following configuration keys have been introduced in version 3:

'legacy_model_binding' => false,
 
'inject_assets' => true,
 
'inject_morph_markers' => true,
 
'navigate' => false,
 
'pagination_theme' => 'tailwind',

You can reference Livewire's new configuration file on GitHub for additional option descriptions and copy-pastable code.

Changed configuration

The following configuration items have been updated with new default values:

New class namespace

Livewire's default class_namespace has changed from App\Http\Livewire to App\Livewire. You are welcome to keep the old namespace configuration value; however, if you choose to update your configuration to the new namespace, you will have to move your Livewire components to app/Livewire:

-'class_namespace' => 'App\\Http\\Livewire',
+'class_namespace' => 'App\\Livewire',

New layout view path

When rendering full-page components in version 2, Livewire would use resources/views/layouts/app.blade.php as the default layout Blade component.

Because of a growing community preference for anonymous Blade components, Livewire 3 has changed the default location to: resources/views/components/layouts/app.blade.php.

-'layout' => 'layouts.app',
+'layout' => 'components.layouts.app',

Removed configuration

Livewire no longer recognizes the following configuration items.

app_url

If your application is served under a non-root URI, in Livewire 2 you could use the app_url configuration option to configure the URL Livewire uses to make AJAX requests to.

In this case, we've found a string configuration to be too rigid. Therefore, Livewire 3 has chosen to use runtime configuration instead. You can reference our documentation on configuring Livewire's update endpoint for more information.

asset_url

In Livewire 2, if your application was served under a non-root URI, you would use the asset_url configuration option to configure the base URL that Livewire uses to serve its JavaScript assets.

Livewire 3 has instead chosen a runtime configuration strategy. You can reference our documentation on configuring Livewire's script asset endpoint for more information.

middleware_group

Because Livewire now exposes a more flexible way to customize its update endpoint, the middleware_group configuration option has been removed.

You can reference our documentation on customizing Livewire's update endpoint for more information on applying custom middleware to Livewire requests.

manifest_path

Livewire 3 no longer uses a manifest file for component autoloading. Therefore, the manifest_path configuration is no longer necessary.

back_button_cache

Because Livewire 3 now offers an SPA experience for your application using wire:navigate, the back_button_cache configuration is no longer necessary.

Livewire app namespace

In version 2, Livewire components were generated and recognized automatically under the App\Http\Livewire namespace.

Livewire 3 has changed this default to: App\Livewire.

You can either move all of your components to the new location or add the following configuration to your application's config/livewire.php configuration file:

'class_namespace' => 'App\\Http\\Livewire',

Discovery

With Livewire 3, there is no manifest present, and there is therefore nothing to “discover” in relation to Livewire Components, and you can safely remove any livewire:discover references from your build scripts without issue.

Page component layout view

When rendering Livewire components as full pages using a syntax like the following:

Route::get('/posts', ShowPosts::class);

The Blade layout file used by Livewire to render the component has changed from resources/views/layouts/app.blade.php to resources/views/components/layouts/app.blade.php:

-resources/views/layouts/app.blade.php
+resources/views/components/layouts/app.blade.php

You can either move your layout file to the new location or apply the following configuration inside your application's config/livewire.php configuration file:

'layout' => 'layouts.app',

For more information, check out the documentation on creating and using a page-component layout.

Eloquent model binding

Livewire 2 supported wire:model binding directly to Eloquent model properties. For example, the following was a common pattern:

public Post $post;
 
protected $rules = [
'post.title' => 'required',
'post.description' => 'required',
];
<input wire:model="post.title">
<input wire:model="post.description">

In Livewire 3, binding directly to Eloquent models has been disabled in favor of using individual properties, or extracting Form Objects.

However, because this behavior is so heavily relied upon in Livewire applications, version 3 maintains support for this behavior via a configuration item in config/livewire.php:

'legacy_model_binding' => true,

By setting legacy_model_binding to true, Livewire will handle Eloquent model properties exactly as it did in version 2.

AlpineJS

Livewire 3 ships with AlpineJS by default.

If you manually include Alpine in your Livewire application, you will need to remove it, so that Livewire's built-in version doesn't conflict.

Including Alpine via a script tag

If you include Alpine into your application via a script tag like the following, you can remove it entirely and Livewire will load its internal version instead:

-<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script>

Including plugins via a script tag

Livewire 3 now ships with the following Alpine plugins out-of-the-box:

It is worth keeping an eye on changes to the package.json file, as new Alpine plugins may be added!

If you have previously included any of these in your application via <script> tags like below, you should remove them along with Alpine's core:

-<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/[email protected]/dist/cdn.min.js"></script>
-<!-- ... -->

Accessing the Alpine global via a script tag

If you are currently accessing the Alpine global object from a script tag like so:

<script>
document.addEventListener('alpine:init', () => {
Alpine.data(...)
})
</script>

You may continue to do so, as Livewire internally includes and registers Alpine's global object like before.

Including via JS bundle

If you have included Alpine or any of the popular core Alpine plugins mentioned above via NPM into your applications JavaScript bundle like so:

// Warning: this is a snippet of the Livewire 2 approach to including Alpine
 
import Alpine from 'alpinejs'
import intersect from '@alpinejs/intersect'
 
Alpine.plugin(intersect)
 
Alpine.start()

You can remove them entirely, because Livewire includes Alpine and many popular Alpine plugins by default.

Accessing Alpine via JS bundle

If you are registering custom Alpine plugins or components inside your application's JavaScript bundle like so:

// Warning: this is a snippet of the Livewire 2 approach to including Alpine
 
import Alpine from 'alpinejs'
import customPlugin from './plugins/custom-plugin'
 
Alpine.plugin(customPlugin)
 
Alpine.start()

You can still accomplish this by importing the Livewire core ESM module into your bundle and accessing Alpine from there.

To import Livewire into your bundle, you must first disable Livewire's normal JavaScript injection and provide the necessary configuration to Livewire by replacing @livewireScripts with @livewireScriptConfig in your application's primary layout:

<!-- ... -->
 
- @livewireScripts
+ @livewireScriptConfig
</body>

Now, you can import Alpine and Livewire into your application's bundle like so:

import { Livewire, Alpine } from '../../vendor/livewire/livewire/dist/livewire.esm';
import customPlugin from './plugins/custom-plugin'
 
Alpine.plugin(customPlugin)
 
Livewire.start()

Notice you no longer need to call Alpine.start(). Livewire will start Alpine automatically.

For more information, please consult our documentation on manually bundling Livewire's JavaScript.

wire:model

In Livewire 3, wire:model is "deferred" by default (instead of by wire:model.defer). To achieve the same behavior as wire:model from Livewire 2, you must use wire:model.live.

Below is a list of the necessary substitutions you will need to make in your templates to keep your application's behavior consistent:

-<input wire:model="...">
+<input wire:model.live="...">
 
-<input wire:model.defer="...">
+<input wire:model="...">
 
-<input wire:model.lazy="...">
+<input wire:model.blur="...">

@entangle

Similar to the changes to wire:model, Livewire 3 defers all data binding by default. To match this behavior, @entangle has been updated as well.

To keep your application running as expected, make the following @entangle substitutions:

-@entangle(...)
+@entangle(...).live
 
-@entangle(...).defer
+@entangle(...)

Events

In Livewire 2, Livewire had two different PHP methods for triggering events:

  • emit()
  • dispatchBrowserEvent()

Livewire 3 has unified these two methods into a single method:

  • dispatch()

Here is a basic example of dispatching and listening for an event in Livewire 3:

// Dispatching...
class CreatePost extends Component
{
public Post $post;
 
public function save()
{
$this->dispatch('post-created', postId: $this->post->id);
}
}
 
// Listening...
class Dashboard extends Component
{
#[On('post-created')]
public function postAdded($postId)
{
//
}
}

The three main changes from Livewire 2 are:

  1. emit() has been renamed to dispatch() (Likewise emitTo() and emitSelf() are now dispatchTo() and dispatchSelf())
  2. dispatchBrowserEvent() has been renamed to dispatch()
  3. All event parameters must be named

For more information, check out the new events documentation page.

Here are the "find and replace" differences that should be applied to your application:

-$this->emit('post-created');
+$this->dispatch('post-created');
 
-$this->emitTo('foo', 'post-created');
+$this->dispatch('post-created')->to('foo');
 
-$this->emitSelf('post-created');
+$this->dispatch('post-created')->self();
 
-$this->emit('post-created', $post->id);
+$this->dispatch('post-created', postId: $post->id);
 
-$this->dispatchBrowserEvent('post-created');
+$this->dispatch('post-created');
 
-$this->dispatchBrowserEvent('post-created', ['postId' => $post->id]);
+$this->dispatch('post-created', postId: $post->id);
-<button wire:click="$emit('post-created')">...</button>
+<button wire:click="$dispatch('post-created')">...</button>
 
-<button wire:click="$emit('post-created', 1)">...</button>
+<button wire:click="$dispatch('post-created', { postId: 1 })">...</button>
 
-<button wire:click="$emitTo('foo', post-created', 1)">...</button>
+<button wire:click="$dispatchTo('foo', 'post-created', { postId: 1 })">...</button>
 
-<button x-on:click="$wire.emit('post-created', 1)">...</button>
+<button x-on:click="$dispatch('post-created', { postId: 1 })">...</button>

emitUp()

The concept of emitUp has been removed entirely. Events are now dispatched using browser events and therefore will "bubble up" by default.

You can remove any instances of $this->emitUp(...) or $emitUp(...) from your components.

Testing events

Livewire has also changed event assertions to match the new unified terminology regarding dispatching events:

-Livewire::test(Component::class)->assertEmitted('post-created');
+Livewire::test(Component::class)->assertDispatched('post-created');
 
-Livewire::test(Component::class)->assertEmittedTo(Foo::class, 'post-created');
+Livewire::test(Component::class)->assertDispatchedTo(Foo:class, 'post-created');
 
-Livewire::test(Component::class)->assertNotEmitted('post-created');
+Livewire::test(Component::class)->assertNotDispatched('post-created');
 
-Livewire::test(Component::class)->assertEmittedUp()

URL query string

In previous Livewire versions, if you bound a property to the URL's query string, the property value would always be present in the query string, unless you used the except option.

In Livewire 3, all properties bound to the query string will only show up if their value has been changed after the page load. This default removes the need for the except option:

public $search = '';
 
protected $queryString = [
- 'search' => ['except' => ''],
+ 'search',
];

If you'd like to revert back to the Livewire 2 behavior of always showing a property in the query string no matter its value, you can use the keep option:

public $search = '';
 
protected $queryString = [
'search' => ['keep' => true],
];

Pagination

The pagination system has been updated in Livewire 3 to better support multiple paginators within the same component.

Update published pagination views

If you've published Livewire's pagination views, you can reference the new ones in the pagination directory on GitHub and update your application accordingly.

Accessing $this->page directly

Because Livewire now supports multiple paginators per component, it has removed the $page property from the component class and replaced it with a $paginators property that stores an array of paginators:

-$this->page = 2;
+$this->paginators['page'] = 2;

However, it is recommended that you use the provided getPage and setPage methods to modify and access the current page:

// Getter...
$this->getPage();
 
// Setter...
$this->setPage(2);

wire:click.prefetch

Livewire's prefetching feature (wire:click.prefetch) has been removed entirely. If you depended on this feature, your application will still work, it will just be slightly less performant in the instances where you were previously benefiting from .prefetch.

-<button wire:click.prefetch="">
+<button wire:click="...">

Component class changes

The following changes have been made to Livewire's base Livewire\Component class that your application's components may have relied on.

The component $id property

If you accessed the component's ID directly via $this->id, you should instead use $this->getId():

-$this->id;
 
+$this->getId();

Duplicate method and property names

PHP allows you to use the same name for both a class property and method. In Livewire 3, this will cause problems when calling methods from the frontend via wire:click.

It is strongly recommended that you use distinct names for all public methods and properties in a component:

-public $search = '';
 
public function search() {
// ...
}
+public $query = '';
 
public function search() {
// ...
}

JavaScript API changes

livewire:load

In previous versions of Livewire, you could listen for the livewire:load event to execute JavaScript code immediately before Livewire initialized the page.

In Livewire 3, that event name has been changed to livewire:init to match Alpine's alpine:init:

-document.addEventListener('livewire:load', () => {...})
+document.addEventListener('livewire:init', () => {...})

Page expired hook

In version 2, Livewire exposed a dedicated JavaScript method for customizing the page expiration behavior: Livewire.onPageExpired(). This method has been removed in favor of using the more powerful request hooks directly:

-Livewire.onPageExpired(() => {...})
 
+Livewire.hook('request', ({ fail }) => {
+ fail(({ status, preventDefault }) => {
+ if (status === 419) {
+ preventDefault()
+ 
+ confirm('Your custom page expiration behavior...')
+ }
+ })
+})

New lifecycle hooks

Many of Livewire's internal JavaScript lifecycle hooks have been changed in Livewire 3.

Here is a comparison of the old hooks and their new syntaxes for you to find/replace in your application:

-Livewire.hook('component.initialized', (component) => {})
+Livewire.hook('component.init', ({ component, cleanup }) => {})
 
-Livewire.hook('element.initialized', (el, component) => {})
+Livewire.hook('element.init', ({ el, component }) => {})
 
-Livewire.hook('element.updating', (fromEl, toEl, component) => {})
+Livewire.hook('morph.updating', ({ el, toEl, component }) => {})
 
-Livewire.hook('element.updated', (el, component) => {})
+Livewire.hook('morph.updated', ({ el, component }) => {})
 
-Livewire.hook('element.removed', (el, component) => {})
+Livewire.hook('morph.removed', ({ el, component }) => {})
 
-Livewire.hook('message.sent', (message, component) => {})
-Livewire.hook('message.failed', (message, component) => {})
-Livewire.hook('message.received', (message, component) => {})
-Livewire.hook('message.processed', (message, component) => {})
 
+Livewire.hook('commit', ({ component, commit, respond, succeed, fail }) => {
+ // Equivalent of 'message.sent'
+ 
+ succeed(({ snapshot, effect }) => {
+ // Equivalent of 'message.received'
+ 
+ queueMicrotask(() => {
+ // Equivalent of 'message.processed'
+ })
+ })
+ 
+ fail(() => {
+ // Equivalent of 'message.failed'
+ })
+})

You may consult the new JavaScript hook documentation for a more thorough understanding of the new hook system.

Localization

If your application uses a locale prefix in the URI such as https://example.com/en/..., Livewire 2 automatically preserved this URL prefix when making component updates via https://example.com/en/livewire/update.

Livewire 3 has stopped supporting this behavior automatically. Instead, you can override Livewire's update endpoint with any URI prefixes you need using setUpdateRoute():

Route::group(['prefix' => LaravelLocalization::setLocale()], function ()
{
// Your other localized routes...
 
Livewire::setUpdateRoute(function ($handle) {
return Route::post('/livewire/update', $handle);
});
});

For more information, please consult our documentation on configuring Livewire's update endpoint.