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

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

3 min read Mohamed Said Mohamed Said

How Livewire v3 Actually Updates the DOM

Most developers treat Livewire as a black box: submit a request, get HTML back, page updates. The reality is more surgical. Livewire v3 uses a morphing algorithm — borrowed conceptually from morphdom — that walks the existing DOM and the incoming HTML simultaneously, patching only what changed.

The key to making this work reliably are morph markers: invisible HTML comments injected around dynamic regions.

<!-- [if BLOCK]><![endif] -->
<div wire:key="item-1">...</div>
<!-- [if ENDBLOCK]><![endif] -->

These markers give the morpher stable anchors so it can match old nodes to new ones without relying solely on element position. If you ever see unexpected flickers or lost focus states, a missing wire:key on a looped element is almost always the culprit — the morpher falls back to positional matching and replaces nodes it could have patched.

Practical Rule

Always add wire:key to every element inside @foreach. Use a domain-meaningful key, not $loop->index:

@foreach($orders as $order)
    <div wire:key="order-{{ $order->id }}">
        {{ $order->reference }}
    </div>
@endforeach

JavaScript Lifecycle Hooks

Livewire v3 exposes a rich JS hook system via Livewire.hook(). This is the correct extension point for third-party integrations, analytics, and custom DOM work — not document.addEventListener('livewire:navigated', ...).

Available Hook Points

| Hook | When it fires | |---|---| | component.init | After a component's JS object is created | | request | Before/after each network round-trip | | commit | Around the server commit lifecycle | | morph | Before/after DOM patching | | element.init | When a DOM element is first processed |

A real-world use case: reinitialise a third-party chart library after every morph without leaking instances.

Livewire.hook('morph.updated', ({ el, component }) => {
    if (el.dataset.chart !== undefined) {
        destroyChart(el);
        initChart(el);
    }
});

The commit hook is particularly useful for optimistic UI patterns — you can intercept the outgoing payload and update local state before the server responds:

Livewire.hook('commit', ({ component, commit, respond, succeed, fail }) => {
    // `commit` contains the outgoing updates
    succeed(({ snapshot, effect }) => {
        // runs after a successful server round-trip
        console.log('New snapshot received for', component.name);
    });
});

Alpine.js Integration: Shared Reactive State

Livewire v3 ships Alpine as a first-class dependency and bridges them through $wire — a reactive proxy that exposes your component's public properties directly to Alpine expressions.

<div x-data="{ open: false }">
    <button @click="open = !open">Toggle</button>

    <span x-show="open" x-text="$wire.status"></span>

    <button @click="$wire.refresh()">Refresh</button>
</div>

$wire.status is live: when the Livewire component updates $status on the server, Alpine's reactivity picks up the change automatically via the morph cycle.

Entangle for Two-Way Binding

For cases where Alpine needs to write back to Livewire state, use $wire.entangle():

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

Pass .live to push changes on every Alpine mutation rather than waiting for the next Livewire request:

$wire.entangle('search').live

Avoid entangling large objects — every change triggers a network round-trip. Entangle scalar values; derive complex state server-side.

Takeaways

  • Always use wire:key on looped elements to give the morpher stable anchors and prevent unnecessary DOM replacement.
  • Livewire.hook() is the correct extension point for JS integrations; prefer morph.updated over broad DOM event listeners.
  • $wire is a reactive proxy — Alpine reads Livewire state without extra glue code.
  • entangle() enables two-way binding but carries a network cost; keep entangled values small and scalar.
  • The commit hook lets you observe and react to the full request/response lifecycle from JavaScript without monkey-patching Livewire internals.

Found this useful?

Frequently Asked Questions

3 questions
Q01 Why does my Alpine component lose state after a Livewire update?
The morpher replaced the DOM node instead of patching it, destroying Alpine's reactive scope. Add a stable `wire:key` to the element so Livewire morphs it in place rather than recreating it.
Q02 When should I use `$wire.entangle()` versus just reading `$wire.property`?
Use `$wire.property` (read-only) when Alpine only needs to display or react to server state. Use `entangle()` when Alpine must write back to the Livewire component — for example, a custom date-picker that needs to push its selected value to the server.
Q03 Is `Livewire.hook()` safe to call before Livewire boots?
Yes. Livewire queues hooks registered before it initialises and replays them once the runtime is ready, so you can safely call `Livewire.hook()` in a script tag that loads before `@livewireScripts`.

Continue reading

More Articles

View all