Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration
#livewire #laravel #alpine #javascript

Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration

3 min read Mohamed Said Mohamed Said

Livewire v3 Internals: Morph Markers, JS Hooks, and Alpine Integration

Livewire v3 rewrote its JavaScript core from scratch. If you have ever wrestled with unexpected DOM resets, Alpine state loss, or mysterious re-renders, the answer is almost always in three places: morph markers, the JS hook system, and the Alpine entangle bridge. Understanding these internals turns debugging from guesswork into precision.


How Morph Markers Work

When a Livewire component re-renders, the server sends back a fresh HTML snapshot. The client-side morpher (@livewire/morph) walks both the old and new DOM trees and patches only what changed — similar to morphdom, but Livewire-aware.

To anchor this diff, Livewire injects invisible HTML comments around dynamic regions:

<!-- [if BLOCK]><![endif] -->
<div>{{ $count }}</div>
<!-- [if ENDBLOCK]><![endif] -->

These markers tell the morpher where a keyed block starts and ends. If you remove them — for example by stripping HTML comments in a CDN or build pipeline — the morpher falls back to a full subtree replacement, which destroys Alpine component state.

Practical rule: never strip HTML comments from Livewire responses. Add an exception in your Vite/webpack HTML minifier:

// vite.config.js
export default defineConfig({
  build: {
    minify: 'terser',
    terserOptions: {
      format: { comments: /\[if (BLOCK|ENDBLOCK)\]/ },
    },
  },
});

The JS Hook System

Livewire exposes a first-class hook API that lets you intercept the full request/response lifecycle from JavaScript. This is the correct place for things like global loading indicators, analytics events, or third-party widget re-initialisation.

import { Livewire } from '../../vendor/livewire/livewire/dist/livewire.esm';

Livewire.hook('request', ({ uri, options, payload, respond, succeed, fail }) => {
    // Mutate outgoing payload
    payload.fingerprint.extra = { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone };

    succeed(({ snapshot, effects }) => {
        // Runs after a successful server round-trip
        console.debug('Component updated:', snapshot.memo.name);
    });

    fail(() => {
        document.dispatchEvent(new CustomEvent('livewire:error'));
    });
});

Available hooks include component.init, element.init, element.updating, element.updated, commit, request, and message. The commit hook is particularly useful — it fires synchronously before the morph, giving you a chance to snapshot third-party widget state and restore it after.

Livewire.hook('commit', ({ component, commit, respond, succeed, fail }) => {
    const chartInstance = Chart.getChart(component.el.querySelector('canvas'));
    const zoom = chartInstance?.getZoomLevel();

    succeed(() => {
        if (zoom) chartInstance?.zoomScale('x', zoom);
    });
});

Alpine Integration: Entangle and $wire

Livewire v3 ships Alpine as a first-class dependency. Every Livewire component automatically gets an Alpine scope, and $wire is a reactive proxy to the server-side component.

<div x-data="{ open: $wire.entangle('modalOpen') }">
    <button @click="open = true">Open</button>
    <div x-show="open">Modal content</div>
</div>

entangle creates a two-way reactive binding. By default it defers the server sync until the next Livewire network request. Pass .live to push changes immediately:

<div x-data="{ query: $wire.entangle('search').live }">
    <input x-model="query" />
</div>

For read-only access without a network round-trip, use $wire.get('property') inside Alpine expressions. It reads the last known snapshot value synchronously.

Avoid this anti-pattern: duplicating state in both x-data and a Livewire property without entangle. You end up with two sources of truth that diverge on every re-render.


Key Takeaways

  • Morph markers are HTML comments — protect them from minifiers or suffer full subtree replacements.
  • Use Livewire.hook('commit') to snapshot and restore third-party widget state around re-renders.
  • $wire.entangle is the correct bridge for two-way Alpine ↔ Livewire state; .live pushes changes immediately.
  • $wire.get() is synchronous and reads the snapshot — use it for read-only Alpine expressions to avoid extra requests.
  • The request hook lets you inject metadata into every outgoing payload without touching PHP.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Why does my Alpine component lose state after a Livewire re-render?
The most common cause is stripped HTML comments. Livewire uses comment-based morph markers to anchor DOM diffing. If your build pipeline removes HTML comments, the morpher falls back to full subtree replacement, destroying Alpine's reactive scope. Whitelist Livewire's `[if BLOCK]` comments in your minifier.
Q02 When should I use `$wire.entangle` versus `$wire.get`?
Use `entangle` when you need two-way synchronisation between an Alpine variable and a Livewire property. Use `$wire.get('prop')` when you only need to read the current snapshot value inside an Alpine expression without triggering a network request.
Q03 What is the difference between the `commit` and `request` hooks?
The `request` hook wraps the full HTTP round-trip and lets you mutate the outgoing payload or react to network-level success/failure. The `commit` hook fires per-component, synchronously around the morph phase, making it the right place to preserve third-party widget state that would otherwise be destroyed by DOM patching.

Continue reading

More Articles

View all