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.
Table of Contents
- 1. The Problem With Component-Local State
- 2. Alpine.store: Setup and Core Principle
- 3. Accessing the Store: $store, Alpine.store() and x-data
- 4. Actions and Getters in the Store
- 5. Practical Patterns: Cart, Notifications, Auth
- 6. Combining the Store With $persist
- 7. Alpine.store in Hyva and Magento 2
- 8. Alpine.store vs. Vuex vs. Redux Compared
- 9. Summary
- 10. FAQ
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?
$store.name or Alpine.store('name').2When to use Alpine.store?
3Multiple stores possible?
cart, preferences, notifications, and so on.4Is Alpine.store reactive?
5Computed values in the store?
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()?
this.$store.name.prop. In JavaScript: Alpine.store('name').prop. Both ways are reactive.8init() hook in the store?
init() method inside the store object is called automatically on registration. Ideal for fetching API data on startup.9Integrating a store into Hyva?
.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.