Ember Services — Shared State and Application Logic
In this tutorial, you will learn about Ember Services. We cover key concepts, practical examples, and best practices to help you master this topic.
Ember services are Singleton objects that persist across route changes and component lifecycles. They manage shared state like user sessions, shopping carts, notification queues, and API clients. Services are injected into routes, components, and other services through Ember's Dependency Injection system.
What You'll Learn
You will learn how to create services, inject them into components and routes, manage shared state with tracked properties, and use services for cross-component communication.
Why It Matters
Without services, passing data between unrelated components requires prop drilling or event buses. Services provide a clean, testable way to share state across the entire application.
Real-World Use
A session service manages authentication state. When the user logs in, the session service updates. Every component that needs auth state injects the session service and reads isAuthenticated. The service also provides login, logout, and token refresh methods.
flowchart TD
A[Session Service] --> B[Route A]
A --> C[Route B]
A --> D[Component X]
A --> E[Component Y]
F[Shopping Cart Service] --> D
F --> G[Route C]
H[Notification Service] --> B
H --> C
H --> D
Creating a Service
ember generate service session
// app/services/session.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class SessionService extends Service {
@tracked user = null;
@tracked accessToken = null;
@tracked isAuthenticated = false;
@action
async login(email, password) {
try {
let response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password }),
headers: { 'Content-Type': 'application/json' }
});
let data = await response.json();
this.user = data.user;
this.accessToken = data.token;
this.isAuthenticated = true;
return { success: true };
} catch (error) {
console.error('Login failed:', error);
return { success: false, error };
}
}
@action
logout() {
this.user = null;
this.accessToken = null;
this.isAuthenticated = false;
}
get isAdmin() {
return this.user?.role === 'admin';
}
}
Injecting Services
Use the @service decorator to inject services into any Ember object.
// app/components/nav-bar.js
import Component from '@glimmer/component';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
export default class NavBarComponent extends Component {
@service session;
@service router;
@service notifications;
get userInitials() {
return this.session.user?.name
?.split(' ')
?.map(n => n[0])
?.join('')
?.toUpperCase();
}
@action
async handleLogin(email, password) {
let result = await this.session.login(email, password);
if (result.success) {
this.notifications.success('Welcome back!');
this.router.transitionTo('dashboard');
} else {
this.notifications.error('Login failed. Check your credentials.');
}
}
@action
handleLogout() {
this.session.logout();
this.router.transitionTo('index');
}
}
Shopping Cart Service Example
// 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 = [];
@tracked discountCode = null;
get itemCount() {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
get subtotal() {
return this.items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
get discount() {
if (!this.discountCode) return 0;
// Apply 10% discount
return this.subtotal * 0.1;
}
get total() {
return this.subtotal - this.discount;
}
@action
addItem(product, quantity = 1) {
let existing = this.items.find(item => item.id === product.id);
if (existing) {
existing.quantity += quantity;
} else {
this.items = [...this.items, {
id: product.id,
name: product.name,
price: product.price,
quantity
}];
}
}
@action
removeItem(productId) {
this.items = this.items.filter(item => item.id !== productId);
}
@action
updateQuantity(productId, quantity) {
let item = this.items.find(item => item.id === productId);
if (item) {
item.quantity = quantity;
}
}
@action
applyDiscount(code) {
this.discountCode = code;
}
@action
clear() {
this.items = [];
this.discountCode = null;
}
}
Notification Service
// app/services/notifications.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class NotificationService extends Service {
@tracked notifications = [];
@action
success(message, duration = 3000) {
this.add(message, 'success', duration);
}
@action
error(message, duration = 5000) {
this.add(message, 'error', duration);
}
@action
warning(message, duration = 4000) {
this.add(message, 'warning', duration);
}
@action
info(message, duration = 3000) {
this.add(message, 'info', duration);
}
add(message, type, duration) {
let id = Date.now();
this.notifications = [...this.notifications, { id, message, type }];
setTimeout(() => {
this.dismiss(id);
}, duration);
}
@action
dismiss(id) {
this.notifications = this.notifications.filter(n => n.id !== id);
}
}
{{! app/components/notification-container.hbs }}
<div class="notification-container {{if this.notifications.notifications.length 'has-notifications'}}">
{{#each this.notifications.notifications as |notification|}}
<div class="notification notification-{{notification.type}}"
role="alert"
{{on "click" (fn this.notifications.dismiss notification.id)}}>
{{notification.message}}
</div>
{{/each}}
</div>
Service Lifecycle and Initialization
// app/services/config.js
import Service from '@ember/service';
import { tracked } from '@glimmer/tracking';
export default class ConfigService extends Service {
@tracked theme = 'light';
@tracked language = 'en';
@tracked features = {};
constructor() {
super(...arguments);
this.loadConfig();
}
async loadConfig() {
try {
let response = await fetch('/api/config');
let config = await response.json();
this.theme = config.theme || 'light';
this.language = config.language || 'en';
this.features = config.features || {};
} catch (error) {
console.warn('Could not load config, using defaults');
}
}
}
Testing Services
// tests/unit/services/cart-test.js
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | cart', function(hooks) {
setupTest(hooks);
test('it starts empty', function(assert) {
let cart = this.owner.lookup('service:cart');
assert.equal(cart.itemCount, 0);
assert.equal(cart.total, 0);
});
test('it adds items', function(assert) {
let cart = this.owner.lookup('service:cart');
cart.addItem({ id: 1, name: 'Widget', price: 10 }, 2);
assert.equal(cart.itemCount, 2);
assert.equal(cart.subtotal, 20);
});
test('it removes items', function(assert) {
let cart = this.owner.lookup('service:cart');
cart.addItem({ id: 1, name: 'Widget', price: 10 }, 1);
cart.addItem({ id: 2, name: 'Gadget', price: 20 }, 1);
cart.removeItem(1);
assert.equal(cart.itemCount, 1);
});
test('it applies discounts', function(assert) {
let cart = this.owner.lookup('service:cart');
cart.addItem({ id: 1, name: 'Widget', price: 100 }, 1);
cart.applyDiscount('SAVE10');
assert.equal(cart.discount, 10);
assert.equal(cart.total, 90);
});
});
Common Mistakes
- Putting component-specific state in services. Services should hold shared state. Local UI state (form inputs, dropdown open/close) belongs in the component.
- Mutating tracked arrays directly. Use spread or
filterto create new arrays. Direct mutation withpushdoes not trigger updates. - Not using
@actionon service methods. Without@action, method bindings may be lost when passed as callbacks. - Over-injecting services. A component that injects 5+ services is doing too much. Delegate logic to dedicated services.
- Not resetting service state on logout. Services persist. When the user logs out, reset all service state to prevent data leakage between sessions.
Practice Questions
- What is a service in Ember?
- How do you inject a service into a component?
- How do services maintain state across route transitions?
- What is the difference between a service and a component?
- Challenge: Create three services:
AuthService(login, logout, token management, user info),CartService(add, remove, clear items, apply coupons), andAnalyticsService(track page views, events, user actions). Wire them together so that logging out clears the cart. Inject all three into aDashboardcomponent.
FAQ
Mini Project
Build a complete application with three services: (1) SessionService — manages authentication, provides user info, persists token to localStorage. (2) NotificationService — queue of notifications with auto-dismiss. (3) ThemeService — manages dark/light theme, persists preference. Create components that use each service. Add a settings panel to switch themes.
What's Next
Now that you understand services, learn Ember Controllers for route-specific logic. Then explore Ember Actions for event handling.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro