Short answer
No — AlpineJS 3.x has no dedicated "re-hydrate from DOM" switch for x-model. The reliable fix is to seed the component state from the server-rendered value yourself during initialization, typically in x-init (or Alpine.data's init()) by reading the element's current value and assigning it to the model property. Alternatively, render the initial value into the x-data expression itself so state and DOM agree from the start.
What is actually happening
The confirmed behavior: x-model is a two-way binding. On initialization it pushes the model value into the element, and afterwards it listens for native input/change events to pull values back. If your model property starts as an empty string while the server rendered value="hello", Alpine's outgoing binding can overwrite the visible value, or — depending on directive timing — leave the DOM showing the server value while the internal state stays empty until the user types. Either way, the DOM is not treated as the source of truth for the model.
The likely explanation for the mismatch you describe is initialization order: the server value exists in the DOM before Alpine starts, but nothing copies it into x-data. This is expected design, not a bug — Alpine assumes the JavaScript state owns the data.
Recommended approaches
1. Render the value into x-data (preferred)
Make the server the source of truth for initial state, not just initial markup:
<div x-data="{ email: '<?= htmlspecialchars($email) ?>' }">
<input type="text" x-model="email" value="<?= htmlspecialchars($email) ?>">
</div>
Now state and DOM agree before Alpine even initializes, so there is no hydration gap. Escape the value appropriately for a JS string context.
2. Copy the DOM value in x-init
If you cannot change how x-data is rendered, reconcile in x-init:
<div x-data="{ email: '' }">
<input type="text" x-model="email" value="[contact removed]"
x-init="email = $el.value">
</div>
If you see timing issues (e.g., the outgoing binding overwriting the input before x-init reads it), defer the read:
x-init="Alpine.nextTick(() => { email = $el.value })"
3. Dispatch a synthetic event (works, but least clean)
x-init="$el.dispatchEvent(new Event('input'))"
This forces x-model to pull the current DOM value in, but it also triggers any other input listeners, so prefer option 1 or 2.
Caveats
- Behavior differs by input type. Checkboxes and radio groups bind to
checked, not value; a server-rendered checked attribute may not map onto the model the same way. Seed the boolean/array state explicitly instead.
- Exact init timing has shifted across Alpine 3.x patch releases. Verify against your pinned version.
x-ignore and global flags solve a different problem (preventing double initialization); they do not synchronize model state.
How to verify
- After page load, inspect the component in devtools:
Alpine.$data(document.querySelector('[x-data]')) — the property should equal the server-rendered value before you type anything.
- Dispatch
new Event('input') on the input and confirm the state does not change; if it does, the model was not hydrated from the server value.
If you are binding checkboxes or radios rather than text inputs, say so — the recommended seeding pattern changes for those.