Alpine.js Stores: usage guide and best practices

Introduction

Alpine.js is a lightweight framework for adding JavaScript behavior directly in HTML. One of its powerful features is stores, reactive state containers. This post breaks down when you should use stores, common usage scenarios and best practices.

What Are Alpine.js Stores?

Stores are global objects that hold reactive data accessible across components. Unlike local x-data state, stores let you share data between unrelated components.

While this can also be achieved by having a "root" component with x-data where components are nested inside of the "root" component, stores provides an improved API to achieve this.

Key Benefits

When Should You Use Stores?

1. Sharing State Between Components

Stores shine when components need to communicate. For example:

For example, we can define a global store to track "flash message" state that will be available to all components on the page:

// Store definition
Alpine.store("flash", {
  isOpen: false,
  toggle() {
    this.isOpen = !this.isOpen;
  },
});

The "flash" store can be used like so in the button component.

<!-- Button component -->
<button x-data @click="$store.flash.toggle()">Toggle Flash</button>

The flash message component can read the $store.flash contents.

<!-- Flash message component -->
<div x-data x-show="$store.flash.isOpen">...</div>

2. Global Application State

Use stores for data used across your entire app, such as:

3. Avoiding Large Root x-data Objects

Attaching an x-data to the root <body> can work similarly to using stores. However, it comes with a slight performance penalty as it means that Alpine will attempt to crawl all the elements inside of <body> looking for Alpine directives.

Instead, we can use stores for global data:

<!-- Before (slower for large pages) -->
<body x-data="{ flash: '' }">
  <div x-show="flash">
    <div x-text="flash"></div>
  </div>
  <!-- lots of other elements -->
</body>

<!-- After (faster) -->
<body>
  <div x-data x-show="$store.app.flash">
    <div x-text="$store.app.flash"></div>
  </div>
  <!-- lots of other elements -->
</body>

When not to use Alpine.js stores

Local component state: Use x-data/$data for state isolated to a single component.

state is application based (so think context api in react, or mobx or redux or any global state management strategy) you probably want to use Store.

If the state is isolated to small component tree, (or even isolated to one page) you probably want to use component state (isolate the state locally, think useState in react)

@danddanddand

Best Practices

1. Organize Stores by Domain

Group related data and functionality into separate stores (e.g., $store.auth, $store.cart). Much like regular x-data attributes, functions can be defined inside of the store definition Alpine.store("myStore", { /* here */ }). These functions act like methods, since their this refers to the store.

For example, we can define an auth Alpine.js store with a user property, a login method and a logout method. login and logout have access to user via this.user.

Alpine.store("auth", {
  user: null,
  login() {
    // do the login...
    // update application state
    this.user = {
      /* data from login response */
    };
  },
  logout() {
    // do the logout...
    // update application state
    this.user = null;
  },
});

2. Use Getters for Derived Data

We've seen how functions can be defined on the store definition object. We can use get <function> ("getter") to output fields that look like regular object properties but are computed based on other fields (derived data).

For example, given a "cart" Alpine.js store, we can compute the total price on the fly based on the items by using a get total() {} getter.

Alpine.store("cart", {
  items: [],
  get total() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  },
});

3. Combine Stores with x-model

$store can be combined with the x-model directive, just like a value in the component's $data:

<input x-model="$store.user.email" type="email" />

4. Avoid Overusing Stores

$store is more about storing application state and not storing data. State can be local to components, or shared between them. You'll use it when you have to share state.

@KevinBatdorf

Stores aren’t a replacement for local state, if some data is only relevant for one component, it doesn't need to be in a store.

If you find that the exact same data is required in multiple components, that's when you should think of using stores.

Common Questions

How do I inspect running Alpine stores on a page?

The Alpine.js Devtools include store inspect & edit functionality.

Are Alpine stores in-memory or persisted eg. in localStorage?

Stores are in-memory by default, this means store contents is not persisted across page refreshes. To persist data, you'll need to integrate the $persist plugin, see persist plugin docs

Conclusion

Alpine.js stores simplify managing global state. It's recommended to use them for cross-component state or app-wide settings, but unless you require these attributes, you should default to local x-data/$data values.

Key Takeaways:

Further Reading: