Alpine.js Store: Global State Without Vuex or Redux
AI generated
x-data
Alpine
Alpine.js · Store · State Management · Hyva
Alpine.js Store
Global State Without Vuex or Redux

Global state in large JavaScript applications quickly leads teams toward Vuex, Redux or Zustand. In Alpine.js projects that is usually overkill. Alpine.store delivers reactive global state that is readable and writable from any component, with no build step, no boilerplate and no new concepts to learn.

14 min read Alpine.store · $store · Reactivity · Actions · Getters Alpine.js 3.x · Hyva · Magento 2

1. The Problem With Component-Local State

Alpine.js components built with x-data are isolated by nature: each component has its own state scope that is not accessible to other components. In most cases that is exactly the desired behavior, encapsulation is a sign of good component design. There are situations, however, where state needs to be read or changed by several independent components at the same time. A cart counter in the header needs to update when a product is added on the product page. A toast notification system needs to be triggerable from any component. An authentication status needs to be visible globally and affect the rendering of multiple components.

Without global state, these requirements are often solved with custom events: one component fires an event, another listens for it. That works, but becomes hard to follow once many components and complex data flows are involved. Alpine.store solves the problem more elegantly: a central store holds the shared state, and every component reads and writes it via $store.storeName.property. When one component changes a store value, Alpine.js automatically updates every other component that references that value in its template. No event system needed, no manual synchronization.

2. Alpine.store: Setup and Core Principle

A store is registered with Alpine.store('name', object). The object can contain any values and methods. Registration must happen in the alpine:init event or before Alpine.start(). After that, the store is available as Alpine.store('name') in JavaScript and as $store.name in every x-data context. Alpine.js's reactivity system makes every store value automatically reactive: templates that reference store properties update immediately on change. That also applies to values changed from plain JavaScript.

A store is a plain JavaScript object, not a proxy, not an observable, not a special class. Alpine.js internally wraps the object in a reactive proxy, similar to reactive() in Vue 3. Methods on the store object have access to this and can read and write store properties. That makes it possible to encapsulate logic directly in the store instead of duplicating it in every component. A store can also have an init() hook that runs when the store is registered.


// Register the store before Alpine.start()
document.addEventListener('alpine:init', () => {

  // Simple counter store
  Alpine.store('counter', {
    count: 0,
    increment() { this.count++; },
    decrement() { this.count = Math.max(0, this.count - 1); },
    reset() { this.count = 0; }
  });

  // Store with an init() hook
  Alpine.store('catalog', {
    products: [],
    loading: false,
    error: null,

    async init() {
      // Runs automatically on registration
      this.loading = true;
      try {
        const res = await fetch('/api/products?featured=1');
        this.products = await res.json();
      } catch (e) {
        this.error = e.message;
      } finally {
        this.loading = false;
      }
    },

    get featuredCount() {
      return this.products.filter(p => p.featured).length;
    }
  });
});

Once registered, stores can be changed from plain JavaScript: Alpine.store('counter').count = 42 triggers the same reactivity chain as a change made inside the store itself. That makes it possible to update stores from legacy code, server push events or async callbacks without that code needing to know anything about Alpine.js, it only needs to know the store's public API.

3. Accessing the Store: $store, Alpine.store() and x-data

In the HTML template, the store is accessed via the magic property $store. It is available in every x-data context, even when the store name was never declared inside that component. $store.cart.count reads the current value, $store.cart.addItem(product) calls a store method. Store access in templates is fully reactive, when cart.count changes, every template that binds to it updates automatically.

From JavaScript, in x-init blocks, event handlers or outside Alpine.js entirely, you use Alpine.store('name'). That returns the reactive store object, which can be read and written. Important: stores are global, there are no store namespaces. On larger projects a naming convention such as cart_v2 or a module prefix is recommended.


// In the HTML template, no x-data scope needed for store access
// <div x-data>
//   <span x-text="$store.cart.count">0</span>
//   <button @click="$store.cart.addItem(product)">Add to cart</button>
// </div>

// Store access from inside Alpine.data()
document.addEventListener('alpine:init', () => {
  Alpine.data('productCard', () => ({
    product: null,
    adding: false,

    async addToCart() {
      this.adding = true;
      // Call a store method
      await Alpine.store('cart').addItem(this.product);
      this.adding = false;
    },

    get isInCart() {
      // Use store state as a computed property
      return Alpine.store('cart').items.some(i => i.id === this.product?.id);
    }
  }));

  // Change the store from outside (no x-data context)
  // Example: server-sent events updating the store
  const evtSource = new EventSource('/api/stock-updates');
  evtSource.onmessage = (e) => {
    const update = JSON.parse(e.data);
    Alpine.store('catalog').updateStock(update.productId, update.qty);
  };
});

4. Actions and Getters in the Store

Well structured stores encapsulate all of their business logic internally. Actions are methods on the store object that change state. Getters are computed values defined in JavaScript as get property() and treated reactively by Alpine.js. This approach keeps components lean: they call store actions and read store getters, but contain no business logic themselves. The pattern mirrors what Vuex calls mutations, actions and getters, without that system's complexity.


document.addEventListener('alpine:init', () => {
  Alpine.store('cart', {
    items: [],
    coupon: null,
    taxRate: 0.19,

    // Actions: change state
    addItem(product, qty = 1) {
      const existing = this.items.find(i => i.id === product.id);
      if (existing) {
        existing.qty += qty;
      } else {
        this.items.push({ ...product, qty });
      }
    },

    removeItem(productId) {
      this.items = this.items.filter(i => i.id !== productId);
    },

    updateQty(productId, qty) {
      const item = this.items.find(i => i.id === productId);
      if (item) {
        qty <= 0 ? this.removeItem(productId) : (item.qty = qty);
      }
    },

    applyCoupon(code, discount) {
      this.coupon = { code, discount };
    },

    clearCart() {
      this.items = [];
      this.coupon = null;
    },

    // Getters: computed values, reactive just like computed in Vue
    get subtotal() {
      return this.items.reduce((sum, i) => sum + i.price * i.qty, 0);
    },

    get discount() {
      return this.coupon ? this.subtotal * this.coupon.discount : 0;
    },

    get tax() {
      return (this.subtotal - this.discount) * this.taxRate;
    },

    get total() {
      return this.subtotal - this.discount + this.tax;
    },

    get count() {
      return this.items.reduce((sum, i) => sum + i.qty, 0);
    },

    get isEmpty() {
      return this.items.length === 0;
    }
  });
});

5. Practical Patterns: Cart, Notifications, Auth

Three use cases show how Alpine.store is applied in practice. The cart store centralizes every cart operation and guarantees that the header badge, the minicart and the checkout button always show the same state. The notification store lets any component trigger toast messages that are rendered from a single, central place in the DOM. The auth store holds authentication status and user data that need to be visible across the whole page, without every component having to check authentication status on its own.

In Hyva projects on Magento 2, stores are typically registered in a dedicated inline script block that is included in the layout via its own .phtml template. That template contains only the store registration code and no HTML output. The call to $hyvaCsp->registerInlineScript() must always follow it, to keep the block compatible with the content security policy.

6. Combining the Store With $persist

Combining Alpine.store with the $persist plugin produces global persistent state: values accessible from every component context that also survive a browser reload. That is the pattern for user preferences, cookie consent and theme settings. The syntax is the same as in a regular component, $persist works directly inside the store object.


document.addEventListener('alpine:init', () => {
  // Global persistent preferences store
  Alpine.store('preferences', {
    theme: Alpine.$persist('light').as('global_theme'),
    language: Alpine.$persist('en').as('global_language'),
    cookieConsent: Alpine.$persist(null).as('cookie_consent_v2'),
    viewMode: Alpine.$persist('grid').as('catalog_view_mode'),

    setTheme(theme) {
      this.theme = theme;
      document.documentElement.dataset.theme = theme;
    },

    acceptAllCookies() {
      this.cookieConsent = {
        analytics: true,
        marketing: true,
        functional: true,
        timestamp: new Date().toISOString()
      };
    },

    rejectOptionalCookies() {
      this.cookieConsent = {
        analytics: false,
        marketing: false,
        functional: true,
        timestamp: new Date().toISOString()
      };
    },

    get hasConsent() {
      return this.cookieConsent !== null;
    },

    get needsCookieBanner() {
      return !this.hasConsent;
    },

    init() {
      // Apply the theme on startup
      if (this.theme) {
        document.documentElement.dataset.theme = this.theme;
      }
    }
  });
});

7. Alpine.store in Hyva and Magento 2

Hyva themes already ship with preconfigured stores provided by the theme itself, for example for the cart, the customer status and the search function. Custom stores complement these without colliding with them, as long as different store names are used. Registering a store in a template referenced from layout XML ensures it is available before any component tries to access it.

A typical Hyva pattern is registering a project-specific store inside a default_head_blocks.xml layout block. That template is loaded in the page <head>, which makes the store instantly available to every component in the body. The $hyvaCsp->registerInlineScript() call after every <script> block is mandatory for CSP compatibility on Magento 2.4.x.

8. Alpine.store vs. Vuex vs. Redux Compared

Comparing Alpine.store, Vuex and Redux makes it clear at which project size and complexity Alpine.store is the right choice. For most Hyva and Magento 2 frontend requirements, Alpine.store is more than enough, and considerably leaner.

Aspect Alpine.store Vuex 4 Redux Toolkit
Bundle size Included in the Alpine bundle (~15 KB) ~30 KB extra ~60 KB extra
Boilerplate Minimal, a single object Separate mutations, actions, getters Slices, reducers, selectors
Build step None needed Recommended (Vite/webpack) Practically mandatory
DevTools integration Limited Vue DevTools, time travel debugging Redux DevTools, full featured
Scalability Good up to roughly 10 to 20 stores Large applications Enterprise scale

9. Summary

Alpine.store is the lean, pragmatic answer to the question of global reactive state in Alpine.js projects. It needs no extra libraries, no build step and no new concept beyond a plain JavaScript object. Actions encapsulate the logic, getters compute derived values, and Alpine.js's reactivity system keeps every component automatically up to date. Combined with the $persist plugin, the store becomes persistent, and combined with custom events it can be integrated into existing Magento 2 systems.

The limits of Alpine.store show up in very large applications with hundreds of store entries, complex asynchronous flows, and a need for time travel debugging. For those scenarios, Vuex or Redux are the right tools. For Hyva themes, Magento 2 frontend development and mid-sized projects built with Alpine.js, Alpine.store is, in most cases, exactly the right tool: neither too little nor too much.

Alpine.js Store: the essentials at a glance

Registration

Alpine.store('name', object) inside the alpine:init event. Stores are instantly accessible in every template via $store.name.

Actions & Getters

Methods act as actions, get property() acts as a reactive getter. Logic belongs in the store, not in the components.

Reactivity

Every change to a store value automatically updates every template that references it, with no manual event system required.

Persistence

$persist used directly inside the store object gives you global persistent state with no extra infrastructure.

Mironsoft

Alpine.js, Hyva themes and Magento 2 frontend development

Need a state architecture for your Hyva project?

We design scalable store structures for Alpine.js projects, from simple preference stores to complex cart and authentication flows in Hyva and Magento 2.

Store design

Designing scalable store structures for complex Magento 2 frontends

Hyva integration

Extending existing Hyva stores and connecting them to custom stores

Migration

Migrating stateful jQuery code to Alpine.store without losing functionality

10. FAQ: Alpine.js Store

1Alpine.store vs. x-data?
x-data is component-local. Alpine.store is global, reachable from any component via $store.name or Alpine.store('name').
2When to use Alpine.store?
For state shared by several independent components: cart, notifications, auth status, theme settings.
3Multiple stores possible?
Yes, as many as you like. A clear naming convention is recommended on growing projects: cart, preferences, notifications, and so on.
4Is Alpine.store reactive?
Fully. Every change, from methods, JS code or async callbacks, automatically updates all referencing templates.
5Computed values in the store?
With JavaScript getters: get total() { return this.subtotal + this.tax; }, Alpine treats them reactively.
6$persist in the store?
Alpine.$persist('light').as('global_theme') used directly in the store gives global persistent state with no boilerplate.
7Calling the store from Alpine.data()?
In the template: this.$store.name.prop. In JavaScript: Alpine.store('name').prop. Both ways are reactive.
8init() hook in the store?
Yes, an init() method inside the store object is called automatically on registration. Ideal for fetching API data on startup.
9Integrating a store into Hyva?
Inline script in a .phtml file inside the alpine:init event. $hyvaCsp->registerInlineScript() after every script. Layout XML in the <head>.
10Changing a store from outside?
Alpine.store('name').property = value from any JS context. Reactivity is triggered, ideal for server-sent events or legacy code.