Services and dependency injection
A service is an ordinary class with logic that several components need: keeping the task list, loading data from a server, working with localStorage. A component does not create the service itself with new — it asks Angular for it, and Angular hands over a ready object. This mechanism is called dependency injection (DI).
Creating a service
ng generate service todo-storeThe CLI creates the file src/app/todo-store.ts with the TodoStore class (before version 20 — todo-store.service.ts and the TodoStoreService class). Let's move everything about tasks into it:
// src/app/todo-store.ts
import { Injectable, computed, signal } from '@angular/core';
import { Todo } from './todo';
@Injectable({ providedIn: 'root' })
export class TodoStore {
private nextId = 1;
readonly todos = signal<Todo[]>([]);
readonly left = computed(() => this.todos().filter((t) => !t.done).length);
add(title: string) {
const todo = { id: this.nextId++, title, done: false };
this.todos.update((list) => [...list, todo]);
}
toggle(id: number) {
this.todos.update((list) =>
list.map((t) => (t.id === id ? { ...t, done: !t.done } : t)),
);
}
remove(id: number) {
this.todos.update((list) => list.filter((t) => t.id !== id));
}
}The @Injectable decorator allows the class to be injected. The providedIn: 'root' setting means there is one service object for the whole application, created on first request.
Getting the service in a component
The inject() function returns the service object. Call it in a field declaration:
// src/app/todo-list/todo-list.ts
import { Component, inject } from '@angular/core';
import { TodoStore } from '../todo-store';
import { TodoItem } from '../todo-item/todo-item';
@Component({
selector: 'app-todo-list',
imports: [TodoItem],
templateUrl: './todo-list.html',
})
export class TodoList {
store = inject(TodoStore);
}<!-- src/app/todo-list/todo-list.html -->
<input #field (keyup.enter)="store.add(field.value)" />
<p>Left: {{ store.left() }}</p>
<ul>
@for (todo of store.todos(); track todo.id) {
<li>
<app-todo-item
[todo]="todo"
(toggle)="store.toggle($event)"
(remove)="store.remove(todo.id)"
/>
</li>
}
</ul>The list works as before, but the component is thinner: it only displays data and passes actions to the service.
One object for everyone
Any other component that calls inject(TodoStore) gets the same object. For example, a counter in the header:
// src/app/header/header.ts
import { Component, inject } from '@angular/core';
import { TodoStore } from '../todo-store';
@Component({
selector: 'app-header',
template: `<header>Open tasks: {{ store.left() }}</header>`,
})
export class Header {
store = inject(TodoStore);
}Add a task in the list — the number in the header changes at once, because both components read the same signal.
A service for part of the application
If you list the service in a component's providers, that component and its children get their own separate object:
// src/app/todo-list/todo-list.ts
@Component({
selector: 'app-todo-list',
providers: [TodoStore],
templateUrl: './todo-list.html',
})This way two to-do lists on one page get independent tasks.
Common mistake
Calling inject() inside an ordinary method, for example in a click handler. Angular throws NG0203: inject() must be called from an injection context. Call inject() only in field declarations or in the constructor.