У попередній частині цієї серії блогів ми розглянули базовий підхід до управління станом в 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.- Тепер компонент стає більш модульним і менш залежним від складного управління станом.
Кожен похідний стан обчислюється в момент потреби і може бути повторно використаний у різних компонентах. - Нижче ми покажемо, як покращити цей процес, щоб у шаблоні була лише одна підписка, оскільки кожен
async
pipe налаштовує свою власну підписку.
Складові потоки з 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