The problem: An Alpine.js x-model bound input updates the DOM value visually, but the underlying Alpine state never actually changes — checks in $watch, conditional rendering, or a submit handler all read the stale/original value.
Environment: Laravel 11, Alpine.js 3.x (via Livewire 3's bundled instance), Blade + Tailwind forms, Vite 5.
The Error
// No console error at all — this is a silent state-sync bug.
// Symptom in console when you manually check state:
$data.email
// -> "" (empty, even though the input clearly shows "user@example.com")
Why It Happens
This almost always comes down to one of three root causes, and all three show up constantly in Laravel + Livewire projects because Blade and Livewire both like to re-render markup:
1. The element got replaced by Livewire, but Alpine's binding didn't reattach. When Livewire morphs the DOM after a network round-trip, if the element with x-model sits inside a Livewire component and the surrounding markup changes shape, Alpine can lose its binding on that specific node even though the input still looks normal.
2. x-model on a custom component without a proper form control. x-model expects a native form element (input, select, textarea) or a component that manually wires up @input/value binding. Slapping x-model directly on a <div> or a custom Blade component wrapper does nothing — Alpine has no event to listen to.
3. Nested x-data scope mismatch. If the input lives inside a nested x-data block (even an empty x-data="{}" from a Blade partial), x-model="email" binds to the nested scope's email, not the parent's — so the parent's copy of email never updates.
The Fix
For cause #3 (the most common one) — check for accidental nested scopes. This typically happens when a Blade partial or component has its own x-data:
<div x-data="{ email: '' }">
<x-input-wrapper>
<!-- if this partial has x-data="{}" on its root, you're now nested -->
<input type="email" x-model="email">
</x-input-wrapper>
</div>
Fix by removing the redundant x-data from the partial, or by explicitly reading from the parent scope with a plain JS reference instead of relying on prototypal fallthrough.
For cause #1 (Livewire morph losing the binding) — add wire:ignore or, better, wire:key with a stable, unique value on the element or its closest repeated wrapper so Livewire doesn't tear down and recreate the node on every render:
<div wire:key="email-field-{{ $user->id }}">
<input type="email" x-model="email" wire:model.defer="email">
</div>
For cause #2 — if you need two-way binding on a non-native element, wire it manually instead of using x-model directly:
<div
x-data="{ email: '' }"
>
<div
contenteditable
@input="email = $event.target.innerText"
x-text="email"
></div>
</div>
Once you know which of the three it is, add a quick sanity check while debugging — x-init="$watch('email', value => console.log('email changed:', value))" on the outer x-data element. If your typing never logs, the binding isn't even reaching that scope.
Step-by-Step
- Open DevTools and add
x-init="$watch('yourField', v => console.log(v))"temporarily on the outerx-dataelement. - Type in the input — if nothing logs, you have a scope mismatch (cause #3). Search the Blade partial tree for a stray
x-databetween your outer scope and the input. - If logging works but the value resets right after a Livewire action, check whether the input sits inside a Livewire component that re-renders — add a stable
wire:keyto the wrapping element. - If
x-modelis on anything other thaninput,select, ortextarea, replace it with a manual@input/x-textbinding as shown above. - Remove your temporary
$watchdebug line once confirmed working. - Rebuild with
npm run buildand retest with a hard refresh.
x-model with wire:model on the same input to keep Alpine and Livewire state in sync, use wire:model.defer (or .blur) — not the default live binding — otherwise every keystroke triggers a Livewire network request that can momentarily overwrite Alpine's local value with a stale server value, which looks identical to a sync bug but is actually a race condition.Still stuck after checking your scope tree? Get in touch via the Contact page and send me your component markup.