derived values without a built-in computed()
Alpine.js has no built-in computed(), but JavaScript getters fill that gap almost completely. Anyone who understands how getters behave reactively inside Alpine.data(), how they can be chained, and how they differ from a regular method, can build derived values that are just as clean as computed properties in Vue or useMemo in React.
Table of Contents
- 1. Why Alpine.js has no built-in computed()
- 2. Getters in Alpine.data() as a computed replacement
- 3. Lazy evaluation and the caching trap with getters
- 4. Using derived values in x-text and x-show
- 5. Chained computed values: a getter using a getter
- 6. Combining computed values with asynchronous data
- 7. Comparison to Vue computed() and React useMemo
- 8. When getters recompute too often
- 9. Getter vs. method vs. stored property
- 10. Summary
- 11. FAQ
1. Why Alpine.js has no built-in computed()
Frameworks like Vue offer computed() as an explicit API for derived, cached values that automatically recompute as soon as one of their dependencies changes. Alpine.js deliberately does without such a dedicated API, because the framework's philosophy is to stay as close to native JavaScript as possible and only introduce its own directives where the language itself does not already offer a solution. For computed values, JavaScript already provides a fitting native tool in getter syntax.
A getter, defined with get name() { return ... } inside an object literal, is read syntactically like a property, but executes the underlying function on every access. Because Alpine.js builds its reactivity on proxies over exactly such object literals, getters inside x-data or Alpine.data() work automatically as reactive values: when a template reads the getter, every reactive property read inside it gets registered as a dependency, exactly like any other reactive expression.
The advantage of this approach: there is no additional learning curve for an Alpine specific API, no import, no extra function. A getter is plain JavaScript and behaves identically whether it is used inside Alpine.js, in a regular class, or in another framework entirely. That is exactly what makes the getter pattern the natural first choice for computed values in Alpine.js.
2. Getters in Alpine.data() as a computed replacement
A simple example clarifies the pattern: a shopping cart with a list of line items should display a total. Instead of manually updating the sum in a separate property on every change, for example through a watcher that resets this.total = ... whenever a line item changes, the sum is defined as a getter. The getter reads the current line item data and computes the sum fresh on every access, there is no separate state that would need to be kept in sync.
document.addEventListener('alpine:init', () => {
Alpine.data('cart', () => ({
items: [
{ name: 'Running shoe', price: 89.9, qty: 1 },
{ name: 'Socks', price: 12.5, qty: 3 }
],
// Computed value via getter — no separate state to keep in sync
get itemCount() {
return this.items.reduce((sum, item) => sum + item.qty, 0)
},
get subtotal() {
return this.items.reduce((sum, item) => sum + item.price * item.qty, 0)
},
get formattedSubtotal() {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(this.subtotal)
}
}))
})
This pattern eliminates an entire class of bugs: there is no way for subtotal to get out of sync, because a watcher was forgotten or an update was missed somewhere in the code. The getter always reads the current state and computes its result from it, there is no second source of truth that could go stale. That is the central advantage of computed values over manually synchronized properties.
3. Lazy evaluation and the caching trap with getters
An important difference between a native JavaScript getter and Vue's computed(): Vue's computed() caches its result internally and only recomputes when a dependency actually changes. A plain JavaScript getter has no built-in caching, it executes its code again on every single read access. For most computed values with simple calculations, like a sum over a handful of line items, this is irrelevant, because the calculation itself takes microseconds.
It becomes problematic when a getter performs an expensive operation, for example sorting a large list or an elaborate string formatting, and this getter is read at multiple places in a template at the same time, for example once in x-text and once in an x-show condition. In that case, the expensive computation runs multiple times per render cycle, because every read access executes the getter's code fresh.
document.addEventListener('alpine:init', () => {
Alpine.data('productTable', () => ({
products: [], // large array with hundreds of entries
// WITHOUT caching: sorts on every single read access
get sortedProducts() {
console.log('sorting...') // fires every time this getter is read
return [...this.products].sort((a, b) => a.price - b.price)
},
// WITH manual caching: only re-sorts when the source array changes
_sortedCache: null,
_sortedCacheKey: '',
get sortedProductsCached() {
const key = JSON.stringify(this.products.map((p) => p.id + ':' + p.price))
if (key !== this._sortedCacheKey) {
this._sortedCacheKey = key
this._sortedCache = [...this.products].sort((a, b) => a.price - b.price)
}
return this._sortedCache
}
}))
})
The manual caching pattern with a key comparison is the pragmatic way to keep an expensive computed value efficient, without pulling in an external library. For most Alpine.js components with manageable data volumes, though, this extra effort is not needed at all, a plain getter without caching is entirely sufficient in the vast majority of cases.
4. Using derived values in x-text and x-show
Getter based computed values can be referenced directly in Alpine directives, exactly like any other property. Since Alpine automatically tracks which reactive properties are read inside an expression while rendering a template, this works correctly even when a getter itself reads several properties. If any one of them changes, the template referencing the getter automatically re-evaluates.
<div x-data="cart()">
<p>Items in cart: <span x-text="itemCount"></span></p>
<p>Subtotal: <span x-text="formattedSubtotal"></span></p>
<!-- Getter as a condition — evaluated like any other reactive expression -->
<div x-show="itemCount === 0" class="text-slate-400">
Your cart is empty.
</div>
<div x-show="subtotal >= 50" class="text-green-700">
Free shipping threshold of 50 reached.
</div>
</div>
This directness is a key reason why the getter pattern works so well in Alpine.js: a computed value needs no special handling in the template, it is referenced exactly like any primitive property. This reduces cognitive load for developers moving between Alpine.js projects, because there is no distinction between a getter property and a regular property at the template level.
5. Chained computed values: a getter using a getter
A particularly powerful pattern is chaining several computed values, where one getter further processes the result of another getter. This mirrors the idea of chained computed properties in Vue and allows complex derivations to be broken down into small, named, individually understandable steps, instead of cramming a single, unwieldy calculation into one getter.
document.addEventListener('alpine:init', () => {
Alpine.data('checkout', () => ({
items: [{ price: 89.9, qty: 1 }, { price: 12.5, qty: 3 }],
taxRate: 0.19,
shippingThreshold: 50,
shippingCost: 4.9,
get subtotal() {
return this.items.reduce((sum, i) => sum + i.price * i.qty, 0)
},
// Chained: reads another getter, not raw state
get shipping() {
return this.subtotal >= this.shippingThreshold ? 0 : this.shippingCost
},
// Chained again: reads two other getters
get tax() {
return (this.subtotal + this.shipping) * this.taxRate
},
get total() {
return this.subtotal + this.shipping + this.tax
}
}))
})
Every individual getter in this chain stays simple and testable on its own. total reads subtotal, shipping and tax, without total itself needing to know how those values were computed. If taxRate changes, the change automatically propagates through the entire chain up to total, because each getter registers the dependencies it actually reads. This kind of chaining is just as natural in Alpine.js as it is in Vue, with no extra API at all.
6. Combining computed values with asynchronous data
A getter itself cannot be async, because a synchronous property access in a template cannot meaningfully resolve a promise. This is an important limit of the getter pattern: as soon as a derived value depends on a fetch result, the result of the asynchronous operation first has to be cached in a regular, reactive property, from which a synchronous getter can then compute its derivation.
document.addEventListener('alpine:init', () => {
Alpine.data('stockChecker', () => ({
rawStockData: [], // populated asynchronously, read synchronously by getters
async init() {
const response = await fetch('/api/stock')
this.rawStockData = await response.json()
},
// Synchronous getter, derived from already-resolved async data
get inStockCount() {
return this.rawStockData.filter((item) => item.quantity > 0).length
},
get isFullyOutOfStock() {
return this.rawStockData.length > 0 && this.inStockCount === 0
}
}))
})
This pattern cleanly separates the asynchronous fetching of raw data, which happens in init() or a dedicated method, from the synchronous derivation, which is expressed as a computed value through a getter. As soon as rawStockData is set, inStockCount and isFullyOutOfStock update automatically, because both getters register their dependency on rawStockData when read, with no need for the asynchronous logic itself to know anything about the derivation.
7. Comparison to Vue computed() and React useMemo
Anyone moving from Vue or React to Alpine.js instinctively looks for an equivalent to computed() or useMemo(). The key conceptual difference: Vue's computed() automatically caches and only invalidates the cache when a dependency actually changes, React's useMemo() does something similar through an explicit dependency array. A plain JavaScript getter in Alpine.js has neither built in, it recomputes on every read access.
In practice this difference is irrelevant for the vast majority of computed values, because simple calculations like sums, formatting, or filtering small lists sit in the microsecond range. Only for computationally intensive derivations over large data volumes does the lack of automatic caching become relevant, and that is exactly when the manual caching pattern from section three applies. Anyone coming from a React or Vue background should know this difference to avoid building complex caching prematurely where a plain getter already suffices.
8. When getters recompute too often
A common misunderstanding: a getter that is read multiple times in a template, for example once in x-text and additionally in a :class binding, gets evaluated separately for each of those read accesses, even within the same render cycle. For trivial calculations this is not a problem, but for an expensive operation like parsing a large JSON string or a complex regex application, this can add up measurably, especially when the component is instantiated multiple times on a single page.
The pragmatic solution is rarely a full caching system, but often simply performing the expensive computation once in init() or a watcher and storing the result in a regular property, while the getter itself only performs a light, fast transformation of that already computed value. This split, expensive computation rarely, light derivation via getter often, is the key to performant computed values in Alpine.js.
9. Getter vs. method vs. stored property
There are three common approaches for derived values in Alpine.js, differing in syntax and behavior. The following table compares them along the most important criteria.
| Criterion | Getter | Method | Stored property + watcher |
|---|---|---|---|
| Template syntax | x-text="total" |
x-text="getTotal()" |
x-text="total" |
| Sync risk | None, always current | None, always current | Forgotten watcher = stale |
| Caching | No automatic caching | No automatic caching | Yes, by definition |
| Parameters possible | No | Yes | Not directly |
| Best for | Simple, frequently read derivations | Derivations with arguments | Expensive computations, rarely changed |
For the vast majority of computed values in Alpine.js components, the getter is the right choice, because it offers the leanest syntax and the lowest error risk. Methods become necessary once a derived value depends on a parameter that is not part of the component state. Stored properties with a watcher only pay off for demonstrably expensive computations that rarely need to be re-evaluated.
Mironsoft
Alpine.js architecture and Hyvä frontend development for Magento
Derived values that are always correct and performant?
We turn fragile, manually synchronized state into clean getter based computed values and identify expensive computations that should be cached deliberately.
State audit
Identify and replace manually synchronized properties
Getter refactoring
Chain computed values cleanly and structure them for testability
Performance tuning
Identify expensive getters and cache them deliberately
10. Summary
Computed values in Alpine.js are built with native JavaScript, not a framework specific API. A getter inside Alpine.data() works automatically as a reactive, derived property thanks to proxy based reactivity, without a second source of truth that would need to be manually synchronized. This eliminates an entire class of bugs that arise from manually maintained, derived properties with watchers.
Getters can be chained freely, so complex derivations can be broken down into small, understandable steps, and they work directly in any Alpine directive like x-text or x-show. The most important limitation: getters have no built-in caching and cannot be asynchronous, which means expensive computations and fetch results have to be handled separately. Knowing these boundaries gives you, with the getter pattern, a simple, robust tool for computed values in every Alpine.js component.
Computed values and the getter pattern in Alpine.js — the essentials at a glance
Getters instead of computed()
get name() { return ... } in Alpine.data() is reactive, thanks to proxy based dependency tracking.
No automatic caching
Every read re-runs the getter's code, add manual caching for expensive computations.
Chaining possible
A getter can read another getter, breaking complex derivations into small steps.
No async getters
Store async data in a regular property first, then derive it synchronously via getter.