У попередній частині цієї серії блогів ми розглянули базовий підхід до управління станом в Angular, використовуючи сервіси та розуміння шаблону проектування Singleton. Ми також ознайомилися з концепцією реактивного стану за допомогою BehaviorSubject та Observable, щоб керувати даними нашого додатка. Тепер ми розширимо свої знання, заглибившись у ще дві концепції: похідний стан та складові потоки з використанням RxJS.
Ці концепції дозволять нам створювати більш підтримувані та масштабовані рішення для управління станом, які здатні обробляти складні потреби додатків.
Що таке похідний стан?
У проектуванні програмного забезпечення похідний стан означає значення, які обчислюються на основі інших частин стану. В контексті нашого додатка Angular, похідний стан можна розглядати як Observable, що обчислюється з одного чи кількох інших Observables. Це корисно, коли певні значення в додатку залежать одне від одного, але ви хочете уникнути їх ручного перерахунку або збереження зайвого стану.
Приклад похідного стану з використанням RxJS
Продовжимо з прикладом CounterService. Припустимо, нам потрібно відобразити похідний стан, наприклад, подвоєне значення лічильника або останнє парне число.
Ми можемо досягти цього за допомогою операторів RxJS, таких як map, combineLatest та інших.
Ось як можна розширити CounterService, щоб експонувати похідні стани:
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class CounterService {
private counterSubject = new BehaviorSubject(0); // Початкове значення лічильника - 1
// Observable для надання стану лічильника
counter$ = this.counterSubject.asObservable();
// Похідний стан: лічильник подвоєний
counterDoubled$ = this.counter$.pipe(map((counter) => counter * 2));
// Похідний стан: останнє парне число
latestEven$ = this.counter$.pipe(filter((counter) => counter % 2 === 0));
increment() {
this.counterSubject.next(this.counterSubject.value + 1);
}
decrement() {
this.counterSubject.next(this.counterSubject.value - 1);
}
reset() {
this.counterSubject.next(0);
}
}
У цьому коді:
counterDoubled$— це похідний стан, який виводить подвоєне поточне значення лічильника.latestEven$— це ще один похідний стан, який виводить останнє парне число.
Тепер, у компоненті, ви можете підписатися на ці Observables і відобразити похідні стани безпосередньо в шаблоні:
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { CounterService } from '../../counter.service';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
counter$: Observable;
counterDoubled$: Observable;
latestEven$: Observable;
constructor(private counterService: CounterService) {
this.counter$ = counterService.counter$;
this.counterDoubled$ = counterService.counterDoubled$;
this.latestEven$ = counterService.latestEven$;
}
increment() {
this.counterService.increment();
}
decrement() {
this.counterService.decrement();
}
reset() {
this.counterService.reset();
}
}
Шаблон виглядатиме так:
Counter Value: {{ counter$ | async }}
Counter Doubled: {{ counterDoubled$ | async }}
Last Even: {{ latestEven$ | async }}
Increment Decrement Reset
У цьому налаштуванні:
counterDoubled$таlastEven$— це похідні стани (derived state), які автоматично оновлюються, коли змінюється оригінальнийcounter$Observable.- Тепер компонент стає більш модульним і менш залежним від складного управління станом.
Кожен похідний стан обчислюється в момент потреби і може бути повторно використаний у різних компонентах. - Нижче ми покажемо, як покращити цей процес, щоб у шаблоні була лише одна підписка, оскільки кожен
asyncpipe налаштовує свою власну підписку.
Складові потоки з RxJS
Ще одна потужна функція RxJS — це можливість об’єднувати кілька потоків стану та операцій разом для створення складних потоків даних у підтримуваний та реактивний спосіб.
Складові потоки дозволяють поєднувати або зливати observables з різних частин вашого додатка та застосовувати перетворення декларативним способом. Використовуючи оператори, такі як combineLatest, withLatestFrom, concat і merge, ви можете легко створювати складну логіку управління станом, яка реагує на кілька подій.
Давайте розширимо наш попередній приклад, додавши складений потік стану, що відображає зміни з трьох observables.
Приклад: Об'єднання кількох потоків
import { Component } from '@angular/core';
import { combineLatest, Observable } from 'rxjs';
import { CounterService } from '../../counter.service';
import { map } from 'rxjs/operators';
interface ViewModel {
counter: number;
counterDoubled: number;
latestEven: number;
}
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent {
vm$: Observable;
constructor(private counterService: CounterService) {
// складений потік даних
this.vm$ = combineLatest([
this.counterService.counter$,
this.counterService.counterDoubled$,
this.counterService.latestEven$,
]).pipe(
map(([counter, counterDoubled, latestEven]) => ({
counter,
counterDoubled,
latestEven,
}))
);
}
increment() {
this.counterService.increment();
}
decrement() {
this.counterService.decrement();
}
reset() {
this.counterService.reset();
}
}
Тепер шаблон виглядатиме так:
Counter Value: {{ vm.counter }}
Counter Doubled: {{ vm.counterDoubled }}
Last Even: {{ vm.latestEven }}
Increment Decrement Reset
Як це працює:
- Композиція стану: Ми об'єднали кілька потоків стану (
counter$,counterDoubled$, таlastEven$) в один потік (vm$). Таким чином, шаблон потребує підписки лише наvm$і автоматично отримує весь необхідний стан. - Оновлення шаблону: Коли будь-який з observables змінюється, це ініціює перерахунок потоку
vm$, і pipeasyncгарантує, що шаблон буде автоматично оновлений.
Переваги цього підходу:
- Єдине джерело правди: Шаблон підписується лише на
vm$— один observable, що агрегує всі релевантні стани, що полегшує управління і знижує ймовірність помилок. - Ефективність: Вид буде перерендерено лише тоді, коли один з об’єднаних observables зміниться (лічильник, подвоєне значення або останнє парне).
- Чистіший код компонента: Компонент більше не потребує підписки на кілька observables або управління окремими частинами стану. Він просто споживає складений стан з різних observables.
У наступній частині цієї серії ми зробимо ще один крок уперед, додавши користувацькі дії до цих observable потоків даних для ініціювання оновлень стану в стилі redux.
Перекладено з: Angular State Managment Made Simple: Part 2