JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Управление состоянием через сервис: пример счетчика/данны...

Управление состоянием через сервис: пример счетчика/данных пользователя

Модуль 4: Node.js, Next.js и Angular
15 уровень , 7 лекция
Открыта

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 компонента, то получите отдельный экземпляр для каждого компонента. Обычно это не то, что вам нужно для общего состояния.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ