В попередній частині цієї серії блогів ми розглянули основний підхід до керування станом в Angular, використовуючи сервіси та патерн Singleton. Ми також ввели концепцію реактивного керування станом за допомогою BehaviorSubject
та Observable
. Тепер ми розширимо цей підхід, реалізуючи патерн, подібний до Redux, в Angular, щоб краще керувати змінами стану та обробляти дії користувача. Цей патерн допоможе досягти більш передбачуваного та масштабованого керування станом в наших Angular додатках.
Що таке Redux?
Redux — це передбачуваний контейнер стану для JavaScript-додатків. Основна концепція Redux полягає в тому, що стан зберігається в єдиному об’єкті і оновлюється шляхом відправлення дій, які обробляються редукторами. У цьому блозі ми імітуємо цей патерн, використовуючи RxJS та реактивні можливості Angular.
Ми введемо дії, редуктори та історію стану для керування взаємодіями користувача з лічильником. Ці взаємодії включатимуть збільшення та зменшення лічильника, а також скасування та повторення змін.
Патерн, подібний до Redux
Ключові концепції:
- Стан: Стан представляє поточні дані в нашому додатку.
- Дії: Дії — це інформаційні пакети, які передають дані від додатка до сховища.
- Редуктори: Редуктори — це чисті функції, які визначають, як стан повинен змінюватися у відповідь на дію.
- Скасування/повторення: Додатки за типом Redux часто мають функціональність скасування та повторення для обробки помилок користувача або відновлення дій.
Давайте почнемо з розробки CounterService, де ми реалізуємо цей патерн.
Крок 1: Визначення стану та дій
Ми почнемо з визначення стану та дій у нашому CounterService
. Стан міститиме count
(значення лічильника), історію попередніх значень (history
) і окрему історію для скасування (undoHistory
), щоб ми могли скасувати та повторити дії.
Інтерфейс CounterState
:
export interface CounterState {
count: number; // Поточне значення лічильника
history: number[]; // Історія значень лічильника
undoHistory: number[]; // Історія для скасування дій
}
Інтерфейс CounterAction
:
export interface CounterAction {
action: 'ADD_COUNT' | 'SUBTRACT_COUNT' | 'UNDO' | 'REDO'; // Типи дій
}
Крок 2: Реалізація CounterService
CounterService
керує станом та надає методи для оновлення стану залежно від різних дій.
Використовується RxJS
's Subject
для відправлення дій, а scan
для накопичення стану з часом.
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
import { scan, startWith } from 'rxjs/operators';
@Injectable({
providedIn: 'root',
})
export class CounterService {
// Початковий стан
initialState = {
count: 0,
history: [0],
undoHistory: [],
} as CounterState;
// Observable для відстеження дій
private action$ = new Subject();
// Ініціалізація стану за допомогою scan для накопичення стану
state$ = this.action$.pipe(
scan(this.reducer, this.initialState),
startWith(this.initialState)
);
// Функція-редуктор для обробки дій та оновлення стану
private reducer(state: CounterState, action: CounterAction): CounterState {
switch (action.action) {
case 'ADD_COUNT':
const addCount = state.count + 1;
return {
...state,
count: addCount,
history: [...state.history, addCount],
};
case 'SUBTRACT_COUNT':
const subtractCount = state.count - 1;
return {
...state,
count: subtractCount,
history: [...state.history, subtractCount],
};
case 'UNDO':
if (state.history.length === 0) {
return state;
}
const lastCounterVal = state.history[state.history.length - 1];
return {
count: lastCounterVal,
history: state.history.slice(0, -1),
undoHistory: [...state.undoHistory, lastCounterVal],
};
case 'REDO':
if (state.undoHistory.length === 0) {
return state;
}
const nextCounterVal = state.undoHistory[state.undoHistory.length - 1];
return {
count: nextCounterVal,
history: [...state.history, nextCounterVal],
undoHistory: state.undoHistory.slice(0, -1),
};
default:
return state;
}
}
// Методи для відправлення дій
add() {
this.action$.next({ action: 'ADD_COUNT' });
}
subtract() {
this.action$.next({ action: 'SUBTRACT_COUNT' });
}
undo() {
this.action$.next({ action: 'UNDO' });
}
redo() {
this.action$.next({ action: 'REDO' });
}
}
Крок 3: Створення компонента
Тепер давайте створимо компонент, який взаємодіятиме з нашим CounterService
. Ми будемо використовувати детекцію змін з OnPush
для оптимізації продуктивності та підписуватися на стан за допомогою async
пайпа в шаблоні.
Це дозволяє шаблону автоматично оновлюватися, коли стан змінюється.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Observable } from 'rxjs';
import { CounterService, CounterState } from './counter.service';
@Component({
selector: 'app-counter',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
Count: {{ counter.count }}
Add Subtract Undo Redo
History: {{ counter.history.join(', ') }}
Undo History: {{ counter.undoHistory.join(', ') }}
`,
})
export class CounterComponent {
vm$: Observable<CounterState>;
constructor(private counterService: CounterService) {
this.vm$ = this.counterService.state$;
}
add() {
this.counterService.add();
}
subtract() {
this.counterService.subtract();
}
undo() {
this.counterService.undo();
}
redo() {
this.counterService.redo();
}
}
Як це працює:
-
Управління станом:
CounterService
відстежує стан, який оновлюється на основі відправлених дій.- Стан накопичується за допомогою
scan
, що дозволяє застосовувати функцію-редуктор і генерувати новий стан на основі поточної дії.
-
Undo/Redo:
- Дії
UNDO
таREDO
дозволяють нам переміщатися через історію змін стану, імітуючи поведінку, знайдену в багатьох додатках на основі Redux. Масивhistory
відстежує всі попередні стани, а масивundoHistory
використовується для збереження станів, які були скасовані.
- Дії
-
Взаємодія з компонентом:
- Компонент підписується на
vm$
(observable стан), який автоматично відображає зміни в поданні завдяки Angularasync
пайпу. - Стратегію детекції змін
OnPush
використовують для оптимізації продуктивності, обмежуючи перевірки змін лише тоді, коли стан явно оновлюється.
- Компонент підписується на
Переваги патерну, подібного до Redux, в Angular:
-
Прогнозованість:
- Використовуючи дії та редуктори, ви централізуєте логіку оновлення стану. Це призводить до більш передбачуваної поведінки, що дозволяє розробникам відлагоджувати та відслідковувати зміни, особливо в складних додатках.
-
Компонованість:
- Цей підхід робить додавання нових дій чи оновлень стану модульним. Ви можете легко розширити цей патерн для більш складних додатків.
Висновок
У цьому блозі ми розширили управління станом в Angular за допомогою патрерну, подібного до Redux, використовуючи RxJS та сервіси Angular. Цей патерн дозволяє нам управляти станом у прогнозований, масштабований спосіб, одночасно обробляючи дії користувача, такі як undo/redo та інші побічні ефекти. Ми також дослідили, як використовувати компоновані потоки для спрощення управління станом та розв'язання складної логіки компонента.
Перекладено з: Implementing a Redux-like Pattern in Angular for State Management