Redux i Anguler

Istnieje kilka implementacji Redux'a dla Angulara (>=2):

  • ngrx - implementacja architektury redux - nie jest to obudowa Redux, ale samodzielna implementacja;
  • ng2-redux - implementacja połączenia z Redux dla Angular 2
  • angular-redux - aktualna implementacja połączenia Reduxa a Angularem >=2

Dalsza część opisu dotyczy angular-redux.

Krótkie wprowadzenie (ang.): https://github.com/angular-redux/store/blob/master/articles/intro-tutorial.md. W języku polskim: https://fsgeek.pl/post/redux-store-w-angularze/

Instalacja

Instalujemy Redux i angular-redux/store (https://github.com/angular-redux/store).

npm install --save redux
npm install --save @angular-redux/store

Przygotowanie store aplikacji

Korzystamy z poprzedniego przykładu. Zmiany są bardzo niewielkie. Przede wszystkim umieszczamy implementację w plikach *.ts (Typescript). Tworzymy katalog src/app/rx. Tworzymy dwa pliki: actions.ts i store.ts:

W pliku action.ts umieszczamy acje. Powinniśmy je umieścić wewnątrz klasy (tu: xoActions):

export class xoActions {
  static ACTION_ZMIANA = 'ACTION_ZMIANA';

  zmiana(x,y : string) {
    return {
      type: xoActions.ACTION_ZMIANA,
      x,
      y
    };
  }
}

W pliku store.ts umieszczamy implemnetację pamięci aplikacji (store). Najważniejszą zmianą jest użycie interfejsów do opisu stanu aplikacji:

import { createStore,  DeepPartial, Store,  compose  } from 'redux';
import { xoActions } from './actions';

export interface IStanAplikacji { // zamiast export const StanAplikacji = {
    xo : string[], // znak x / o
    kto : string,  // czyj ruch
    stanGry: string// komunikat
  }

export const INITIAL_STATE: IStanAplikacji  = {
    xo: new Array(9).fill('?'),
    kto: 'x',
    stanGry: '* Kółko i Krzyżyk *'
}

export const reducer = (state=INITIAL_STATE, action) => {
    if (action.type === xoActions.ACTION_ZMIANA) {
    let ix = parseInt(action.y,10)*3 + parseInt(action.x,10);
    if (state.xo[ix] !== '?') return state;
    let nxo  = state.xo.slice(); // PŁYTKA KOPIA state.xo.
    nxo[ix] = state.kto;
    let nkto='x';
    let nstan = 'ruch: x';
    if (state.kto==='x') {
      nkto='o';
      nstan = 'ruch: o';
    }
    if (   ((nxo[0] !== '?') && (nxo[0]===nxo[1]) && (nxo[0]===nxo[2]))
         || ((nxo[3] !== '?') && (nxo[3]===nxo[4]) && (nxo[3]===nxo[5]))
         || ((nxo[6] !== '?') && (nxo[6]===nxo[7]) && (nxo[6]===nxo[8]))
         || ((nxo[0] !== '?') && (nxo[0]===nxo[3]) && (nxo[0]===nxo[6]))
         || ((nxo[1] !== '?') && (nxo[1]===nxo[4]) && (nxo[1]===nxo[7]))
         || ((nxo[2] !== '?') && (nxo[2]===nxo[5]) && (nxo[2]===nxo[8]))
         || ((nxo[0] !== '?') && (nxo[0]===nxo[4]) && (nxo[0]===nxo[8]))
         || ((nxo[2] !== '?') && (nxo[2]===nxo[4]) && (nxo[2]===nxo[6]))  ) { 
          nstan = 'KONIEC';
    }
    return {
     ...state, // spread operator https://redux.js.org/recipes/using-object-spread-operator
      xo : nxo,
      kto : nkto,
      stanGry : nstan
    };

  } else {
     return state;
  }
}

export const store = createStore(reducer);

Definicja modułu powiązanego z Redux

Definicja modułu (plik app.module.ts) musi zostać uzupełniona w polu providers, gdzie umieszczamy klasę akcji (tu: xoActions). Z kolei obiekt pamięci "rejestrujemy" poleceniem NgRedux.provideStore (obiekt NgRedux jest parametrem konstruktora):

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { XoComponent } from './xo.component';


// redux
import { NgReduxModule, NgRedux } from '@angular-redux/store';
import { IStanAplikacji, store } from './rx/store';
import { xoActions } from './rx/actions';
//-

@NgModule({
  declarations: [
    AppComponent,
    XoComponent
  ],
  imports: [
    BrowserModule,
    NgReduxModule, // redux
  ],
  providers: [xoActions], // <- redux actions types
  bootstrap: [AppComponent, XoComponent]
})
export class AppModule { 

// redux
constructor(ngRedux: NgRedux<IStanAplikacji> ) {
   ngRedux.provideStore(store);
}
//-

}

Zamiast rejestrowania gotowego store, możemy go w konstruktorze utworzyć : ngRedux.configureStore(reducer);

Użycie store w komponentach

Najprostszym sposobem powiązania store z wyświetlanymi polami komponentów jest przepisanie wartości (w naszym przypadku xo i stanGry). Wtedy wystarczy odpowiednio zdefiniować obsługę zdarzeń i konstruktor . Wykorzystujemy getState().

import { Component } from '@angular/core';
// redux
import { NgRedux } from '@angular-redux/store';
import { xoActions } from './rx/actions';
import { IStanAplikacji, store } from "./rx/store";
//-

@Component({
  selector: 'xo-selector',
  templateUrl: './xo.component.html',
  styleUrls: ['./xo.component.css']
})
export class XoComponent {
  stanGry: string ='KÓŁKO I KRYŻYK';
  public xo: Array<string>;

constructor(                           
  private ngRedux: NgRedux<IStanAplikacji>,
  private actions: xoActions) {  
  this.xo = store.getState().xo; 
  this.stanGry = store.getState().stanGry;
}

public click(event, n, item) {
  const mapxy = [{x:'0',y:'0'},{x:'1',y:'0'},{x:'2',y:'0'},
                 {x:'0',y:'1'},{x:'1',y:'1'},{x:'2',y:'1'},
                 {x:'0',y:'2'},{x:'1',y:'2'},{x:'2',y:'2'}];
 this.ngRedux.dispatch(this.actions.zmiana(mapxy[n].x,mapxy[n].y));
 this.xo = store.getState().xo;
 this.stanGry = store.getState().stanGry;
}

}

To rozwiązanie ma jedną zasadniczą wadę - dane ze store są powielane w polach komponentu. Można ten problem rozwiązać, stosując dekorator własności @select (https://angular-redux.github.io/store/globals.html#select\, https://github.com/angular-redux/store/blob/master/articles/select-pattern.md\)

Na przykład dla pola stanGry możemy zastosować uproszczony zapis:

export class XoComponent {
  @select() 'stanGry' ;
  ....

We wzorcach zamiast {{ stanGry }}użyjemy {{stanGry | async}}