Livewire's @entangle: Two-Way State Sync with Alpine.js Without the Glue Code
Livewire 3's @entangle directive syncs a server-side property with Alpine.js state in both directions — no manual events. Here's a working example, the network cost, and how to verify it.
02 Jul 2025, 18:20 UTC

The problem: two sources of truth
Livewire keeps component state on the server. Alpine.js keeps UI state in the browser. The moment you need both — say, a search input whose value drives a server query and controls a client-side dropdown — you end up writing glue: custom events, $dispatch calls, listeners on both sides. It works, but it's the kind of code that drifts out of sync the first time someone refactors one half.
Livewire 3's @entangle directive removes that glue. It binds a Livewire public property directly to an Alpine variable so both sides always read the same value. This post shows where it fits, a working example, and the performance trade-off you need to plan for.
What @entangle actually does
Inside a Livewire component's Blade view, you can write:
<div x-data>
<input type="text" x-model="@entangle('searchTerm')">
</div>Livewire compiles this into a small JavaScript bridge. When the input changes, Alpine pushes the new value to Livewire, which sends it to the server over AJAX and updates $searchTerm. When the server re-renders and the property changes there, the update flows back into Alpine. One value, two consumers, no manual events.
Version assumptions: this requires Livewire 3.x and Alpine.js 3.x (Alpine ships bundled with Livewire 3). Livewire 2 used @entangle differently and the syntax here won't behave the same way — check composer show livewire/livewire before copying examples.
A worked example: instant search with a client-side clear button
The component class (run php artisan make:livewire UserSearch from your Laravel project root; requires write access to the app):
class UserSearch extends Component
{
public string $searchTerm = '';
public function render()
{
return view('livewire.user-search', [
'users' => User::where('name', 'like', "%{$this->searchTerm}%")
->limit(10)->get(),
]);
}
}The view — note the Alpine-side button can clear the same property without any event wiring:
<div x-data>
<div class="relative">
<input
type="text"
x-model="@entangle('searchTerm')"
placeholder="Search users..."
>
<button x-show="$wire.searchTerm.length > 0"
x-on:click="$wire.searchTerm = ''">
Clear
</button>
</div>
<ul>
@foreach ($users as $user)
<li>{{ $user->name }}</li>
@endforeach
</ul>
</div>Typing in the input updates the server property and re-runs the query. Clicking Clear sets the property from Alpine's side, which also triggers the server round-trip and empties the results. Neither side knows the other exists — that's the point.
The trade-off: every keystroke is a network request
Two-way sync via @entangle is not free. Each change to the entangled property triggers a Livewire update — a POST to the server and a re-render. For a search box, that means a request per keystroke, which is wasteful and can produce visible flicker on slow connections.
The fix is to keep the entangled value local until the user pauses. The cleanest way in Livewire 3 is to add a .live.debounce modifier on the property update rather than entangling raw:
<input type="text"
x-model="@entangle('searchTerm').live"
wire:loading.attr="disabled">Or better, debounce at the Livewire layer with a dedicated input using wire:model.live.debounce.500ms="searchTerm" and reserve @entangle for the pieces Alpine genuinely needs to react to — like the Clear button's x-show condition. A reasonable rule of thumb:
- Entangle when Alpine must read or write the value reactively (toggling UI, coordinating widgets, sharing state with a JS library like a datepicker or map).
- Use wire:model when only the server cares about the value.
- Debounce anything driven by typing.
How to verify it's working
You don't need to trust the abstraction — you can watch it:
- Open browser devtools → Network tab, filter by
livewire. Each change to the entangled property should produce one POST to the Livewire update endpoint. If you see a request per keystroke, add debouncing. - In the Livewire devtools (or a temporary
<div>{{ $searchTerm }}</div>in the view), confirm the server-side property matches what Alpine shows. - Test the reverse direction: change the property server-side (e.g., a
resetFilters()method) and confirm the input clears in the browser without a page refresh.
Closing takeaway
@entangle is the right tool when state genuinely lives in both worlds — Alpine needs to react to it and the server needs to persist or query on it. Reach for it deliberately, pair it with debouncing for text input, and verify the request pattern in devtools before shipping. If only one side ever touches the value, plain wire:model is simpler and cheaper.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.