Most Apps Don't Need Redux — Use a Service
State Management With Services
A service with tracked state is Ember's idiomatic global store. Combine with getters and tasks for everything most apps need.
What you'll learn
- Build a cart service with tracked items
- Expose getters for derived state
- Update via action methods
In React-land, global state often means Redux or Zustand. In Ember, the
idiomatic answer is simpler: a service with @tracked fields. You already
have DI, reactivity, and singletons — that’s all a state library is.
A Cart Service
// app/services/cart.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class CartService extends Service {
@tracked items = [];
get count() {
return this.items.length;
}
get total() {
return this.items.reduce((sum, i) => sum + i.price, 0);
}
@action
add(item) {
this.items = [...this.items, item];
}
@action
remove(id) {
this.items = this.items.filter((i) => i.id !== id);
}
@action
clear() {
this.items = [];
}
} Use From Components
import Component from '@glimmer/component';
import { service } from '@ember/service';
export default class CartBadge extends Component {
@service cart;
} <span>Cart: {{this.cart.count}}</span>
{{#each this.cart.items as |item|}}
<CartRow @item={{item}} @onRemove={{this.cart.remove}} />
{{/each}} Async + State
When mutations are async, pair the service with an ember-concurrency task:
import { task } from 'ember-concurrency';
saveTask = task({ drop: true }, async () => {
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify(this.items),
});
}); You get loading flags (this.cart.saveTask.isRunning) for free in templates.
When To Reach For More
If you find yourself wanting time-travel debugging, undo, or rich
persistence, look at ember-data (for entities) or tracked-toolbox /
ember-data-resources (for derived/async patterns). Most apps never need
anything heavier.