How to Share State Between Alpine.js Components in Laravel

The problem: Two separate x-data components on the same page (say, a cart icon in the navbar and a cart panel in the sidebar) need to reflect the same state, but each x-data block creates its own isolated scope — updating one doesn't update the other.

Environment: Laravel 11, Alpine.js 3.x (Livewire-bundled), Blade layouts split across multiple partials/components, Vite 5.

The Error

// No thrown error — this is a state-isolation problem, not a crash.
// Symptom: clicking "Add to cart" in one component updates
// that component's counter, but a badge in a completely
// different x-data block stays at its old value.

cartCount // -> 3   (inside component A)
cartCount // -> 0   (inside component B, same page, never updated)

Why It Happens

Every x-data="{ ... }" declaration creates its own isolated reactive scope. That's intentional — it's what lets you drop the same Alpine component twice on a page without them fighting over state. But it also means two unrelated x-data blocks have zero awareness of each other by default, even if they're both rendered from the same Blade layout and conceptually represent "the same" piece of data.

People often try to fix this by passing data down through Blade includes, but Blade only renders HTML server-side — it has no way to keep two already-rendered, independent Alpine scopes in sync client-side afterward. You need something that lives outside any single x-data block: that's what Alpine.store() is for.

The Fix

Register a global store once, in your Alpine init hook. Since Livewire 3 bundles Alpine, hook into alpine:init rather than calling Alpine.start() yourself:

// resources/js/app.js
document.addEventListener('alpine:init', () => {
    Alpine.store('cart', {
        items: [],
        count: 0,

        add(item) {
            this.items.push(item);
            this.count++;
        },

        remove(index) {
            this.items.splice(index, 1);
            this.count--;
        }
    });
});

Then reference $store.cart from any component anywhere on the page — no props, no events needed to keep them in sync:

<!-- Navbar badge, resources/views/layouts/navigation.blade.php -->
<span x-data x-text="$store.cart.count"></span>

<!-- Sidebar cart panel, a completely different Blade partial -->
<div x-data>
    <template x-for="(item, index) in $store.cart.items" :key="index">
        <div x-text="item.name"></div>
    </template>
    <button @click="$store.cart.add({ name: 'New Item' })">
        Add
    </button>
</div>

Both blocks read and write the exact same underlying object, so updating the store from either one updates the other instantly — no manual event dispatching required.

Step-by-Step

  1. In resources/js/app.js, add a document.addEventListener('alpine:init', () => { ... }) block if you don't already have one.
  2. Inside it, call Alpine.store('yourStoreName', { ...initial state and methods... }).
  3. Rebuild assets with npm run build (or restart npm run dev).
  4. In any Blade partial, add a minimal x-data (even an empty one, just to give Alpine a scope to attach to) and reference the store as $store.yourStoreName.property.
  5. Call store methods directly from event handlers, e.g. @click="$store.cart.add(item)", instead of duplicating logic per component.
  6. Confirm both components update together by triggering a change in one and watching the other.
Edge case: If a component using $store.cart lives inside a Livewire component that re-renders, the store itself survives (it's global, not tied to any DOM node) — but double-check you're not also duplicating the same state inside Livewire's own PHP-side properties. Keeping cart count in both an Alpine store and a Livewire public property is a common way to end up with two sources of truth that quietly drift apart after a page navigation.

Want help deciding whether a given piece of state belongs in an Alpine store or a Livewire property? Reach out on the Contact page.

Advertisement
Advertisement