Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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-store

The 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.

Thanks for reading!

Donate to the site — we work hard for you.