1. Счетчик на сервисе
В Angular компонент — это как актёр в театре: он может играть свою роль, но не должен таскать с собой все реквизиты и сценарии для других актёров. Если у вас есть общие данные (например, счетчик лайков, пользовательские настройки, корзина покупок), хранить их внутри компонента — плохая идея. Ведь как только компонент исчезнет со сцены (будет уничтожен), все его данные исчезнут вместе с ним.
Вот тут на сцену и выходят сервисы! Они хранят данные вне компонентов, живут столько, сколько нужно (обычно — пока работает приложение) и позволяют делиться этими данными между разными частями приложения.
Давайте разберёмся на практике. Представим, что у нас есть простое приложение-счетчик. Мы хотим, чтобы разные компоненты могли увеличивать, уменьшать и отображать текущее значение счетчика. Всё это должно работать синхронно: если в одном компоненте счетчик увеличился, в другом он сразу же обновился.
Создаём сервис счетчика
Сначала создадим сервис через Angular CLI (или вручную):
ng generate service counter
В файле counter.service.ts напишем:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root' // сервис будет синглтоном на всё приложение
})
export class CounterService {
private count = 0;
getValue(): number {
return this.count;
}
increment(): void {
this.count++;
}
decrement(): void {
this.count--;
}
reset(): void {
this.count = 0;
}
}
Что тут происходит?
- Поле count приватное — никто не может его поменять напрямую.
- Методы getValue, increment, decrement, reset — публичные, их можно вызывать из компонентов.
Используем сервис в компоненте
Теперь давайте внедрим наш сервис в компонент. Например, в counter.component.ts:
import { Component } from '@angular/core';
import { CounterService } from '../counter.service';
@Component({
selector: 'app-counter',
template: `
<h2>Счётчик: {{ counterValue }}</h2>
<button (click)="increment()">+1</button>
<button (click)="decrement()">-1</button>
<button (click)="reset()">Сброс</button>
`
})
export class CounterComponent {
constructor(private counterService: CounterService) {}
get counterValue(): number {
return this.counterService.getValue();
}
increment() {
this.counterService.increment();
}
decrement() {
this.counterService.decrement();
}
reset() {
this.counterService.reset();
}
}
Важный момент:
Мы используем геттер counterValue, чтобы всегда получать актуальное значение из сервиса.
Реактивность: как обновить значение во всех компонентах?
Проблема: если у нас несколько компонентов, отображающих счетчик, Angular не узнает, что значение изменилось, если просто менять примитивное поле.
Решение: использовать реактивный подход — например, через BehaviorSubject из RxJS.
2. Реактивное состояние через сервис (RxJS)
Почему нужен RxJS?
Если вы хотите, чтобы все подписчики (компоненты) автоматически обновлялись при изменении значения — используйте поток данных (observable).
В Angular для этого идеально подходит BehaviorSubject из библиотеки RxJS.
Переделываем сервис на реактивный стиль
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CounterService {
// BehaviorSubject хранит последнее значение и позволяет подписываться на изменения
private countSubject = new BehaviorSubject<number>(0);
// Observable только для чтения — наружу не отдаём сам subject!
count$: Observable<number> = this.countSubject.asObservable();
increment(): void {
this.countSubject.next(this.countSubject.value + 1);
}
decrement(): void {
this.countSubject.next(this.countSubject.value - 1);
}
reset(): void {
this.countSubject.next(0);
}
}
Используем сервис с подпиской в компоненте
import { Component } from '@angular/core';
import { CounterService } from '../counter.service';
@Component({
selector: 'app-counter',
template: `
<h2>Счётчик: {{ count | async }}</h2>
<button (click)="increment()">+1</button>
<button (click)="decrement()">-1</button>
<button (click)="reset()">Сброс</button>
`
})
export class CounterComponent {
count = this.counterService.count$;
constructor(private counterService: CounterService) {}
increment() {
this.counterService.increment();
}
decrement() {
this.counterService.decrement();
}
reset() {
this.counterService.reset();
}
}
Пояснение:
- count$ — поток (observable), мы подписываемся на него через пайп async в шаблоне.
- Теперь любое изменение значения в сервисе автоматически обновит все компоненты, которые используют этот сервис.
3. Пример: сервис с пользовательскими данными
Счётчик — это хорошо, но что делать, если нам нужно хранить не одно число, а сложную структуру данных, например, профиль пользователя?
Описываем модель пользователя
export interface User {
id: number;
name: string;
email: string;
}
Сервис для управления пользователем
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { User } from './user.model';
@Injectable({
providedIn: 'root'
})
export class UserService {
private userSubject = new BehaviorSubject<User | null>(null);
user$: Observable<User | null> = this.userSubject.asObservable();
setUser(user: User) {
this.userSubject.next(user);
}
clearUser() {
this.userSubject.next(null);
}
}
Используем сервис в компоненте
import { Component } from '@angular/core';
import { UserService } from '../user.service';
import { User } from '../user.model';
@Component({
selector: 'app-profile',
template: `
<div *ngIf="user$ | async as user; else noUser">
<h2>Профиль пользователя</h2>
<p>Имя: {{ user.name }}</p>
<p>Email: {{ user.email }}</p>
<button (click)="logout()">Выйти</button>
</div>
<ng-template #noUser>
<p>Пользователь не авторизован</p>
</ng-template>
`
})
export class ProfileComponent {
user$ = this.userService.user$;
constructor(private userService: UserService) {}
logout() {
this.userService.clearUser();
}
}
Где задавать пользователя?
Например, после успешного логина:
// где-то в LoginComponent
this.userService.setUser({ id: 1, name: 'Вася', email: 'vasya@example.com' });
4. Схема: как работает сервис состояния
graph TD A[Component A] -- inject --> S((Service)) B[Component B] -- inject --> S S -- observable --> A S -- observable --> B>
- Оба компонента используют один и тот же сервис (синглтон).
- Сервис хранит данные и раздаёт их через observable.
- Любое изменение данных в сервисе автоматически становится доступным всем подписчикам.
5. Типичные ошибки при управлении состоянием через сервис
Ошибка №1: Хранить состояние только в компоненте.
Если вы храните данные только в компоненте, то при переходе между страницами или перерисовке компонента данные теряются. Сервисы решают эту проблему.
Ошибка №2: Не использовать реактивный подход.
Если вы просто храните примитивы в сервисе и не используете RxJS, то Angular не узнает об изменениях, и другие компоненты не обновятся. Используйте BehaviorSubject и async пайп!
Ошибка №3: Делиться самим subject-ом.
Никогда не отдавайте наружу сам subject, только observable (asObservable()). Иначе любой компонент сможет изменить ваши данные напрямую, и вы потеряете контроль.
Ошибка №4: Лишние подписки и утечки памяти.
Если вы подписываетесь на observable вручную (через subscribe()), не забывайте отписываться (ngOnDestroy). Если используете async пайп — Angular сам всё сделает.
Ошибка №5: Создавать сервис с providedIn: 'any' или на уровне компонента, когда нужен синглтон.
Если вы случайно указали providedIn: 'any' или добавили сервис в providers компонента, то получите отдельный экземпляр для каждого компонента. Обычно это не то, что вам нужно для общего состояния.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ