Реалізація патерну, подібного до Redux, в Angular для управління станом

В попередній частині цієї серії блогів ми розглянули основний підхід до керування станом в Angular, використовуючи сервіси та патерн Singleton. Ми також ввели концепцію реактивного керування станом за допомогою BehaviorSubject та Observable. Тепер ми розширимо цей підхід, реалізуючи патерн, подібний до Redux, в Angular, щоб краще керувати змінами стану та обробляти дії користувача. Цей патерн допоможе досягти більш передбачуваного та масштабованого керування станом в наших Angular додатках.

Що таке Redux?

Redux — це передбачуваний контейнер стану для JavaScript-додатків. Основна концепція Redux полягає в тому, що стан зберігається в єдиному об’єкті і оновлюється шляхом відправлення дій, які обробляються редукторами. У цьому блозі ми імітуємо цей патерн, використовуючи RxJS та реактивні можливості Angular.

Ми введемо дії, редуктори та історію стану для керування взаємодіями користувача з лічильником. Ці взаємодії включатимуть збільшення та зменшення лічильника, а також скасування та повторення змін.

Патерн, подібний до Redux

Ключові концепції:

  1. Стан: Стан представляє поточні дані в нашому додатку.
  2. Дії: Дії — це інформаційні пакети, які передають дані від додатка до сховища.
  3. Редуктори: Редуктори — це чисті функції, які визначають, як стан повинен змінюватися у відповідь на дію.
  4. Скасування/повторення: Додатки за типом 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 стан), який автоматично відображає зміни в поданні завдяки Angular async пайпу.
    • Стратегію детекції змін OnPush використовують для оптимізації продуктивності, обмежуючи перевірки змін лише тоді, коли стан явно оновлюється.

Переваги патерну, подібного до Redux, в Angular:

  1. Прогнозованість:

    • Використовуючи дії та редуктори, ви централізуєте логіку оновлення стану. Це призводить до більш передбачуваної поведінки, що дозволяє розробникам відлагоджувати та відслідковувати зміни, особливо в складних додатках.
  2. Компонованість:

    • Цей підхід робить додавання нових дій чи оновлень стану модульним. Ви можете легко розширити цей патерн для більш складних додатків.

Висновок

У цьому блозі ми розширили управління станом в Angular за допомогою патрерну, подібного до Redux, використовуючи RxJS та сервіси Angular. Цей патерн дозволяє нам управляти станом у прогнозований, масштабований спосіб, одночасно обробляючи дії користувача, такі як undo/redo та інші побічні ефекти. Ми також дослідили, як використовувати компоновані потоки для спрощення управління станом та розв'язання складної логіки компонента.

Перекладено з: Implementing a Redux-like Pattern in Angular for State Management

Leave a Reply

Your email address will not be published. Required fields are marked *