Sposobów połączenia Reduksa z Angularem 2 jest wiele, między innymi biblioteka ng2-redux, czy bardziej skomplikowany koncept łączący FRP i Reduksa: ngrx/store. Tutaj prezentuję najprostszą metodę, dzięki czemu wiedza ta jest najbardziej uniwersalna.
Koncepcja
Napiszmy teraz znowu listę zadań w Angular 2, tym razem wykorzystując Reduksa do zarządzania stanem aplikacji. Zacznijmy od zaprojektowania kodu i podziału na komponenty. Potrzebujemy komponent do dodawania zadań, komponent reprezentujący ich listę oraz komponent będący pojedynczym zadaniem na liście. Koncepcyjnie HTML będzie wyglądać tak:
<my-app>  
    <my-add-todo></my-add-todo>
    <my-todo-list>
        <my-todo-list-item></my-todo-list-item>
        <my-todo-list-item></my-todo-list-item>
        …
    </my-todo-list>
</my-app>  
Redux
Akcje
Rozpoczynamy od stworzenia projektu oraz definiujemy akcje Reduksa w pliku todoActions.ts. W tej prostej appce potrzebujemy tylko dwóch akcji:
- utworzenie zadania
 - oznaczenie zadania jako ukończone/nieukończone
 
Oprócz tego, w tym samym pliku umieszczamy też klasę z metodami ułatwiającymi tworzenie akcji (tzw. action creators):
export const ADD_TODO = 'ADD_TODO';  
export const TOGGLE_TODO = 'TOGGLE_TODO';
@Injectable()
export class TodoActions {  
  private nextTodoID = 0;
  addTodo(title:string) {
    return {
      id: this.getNextID(),
      type: ADD_TODO,
      title,
      complete: false
    };
  }
  toggleTodo(id:number) {
    return {
      id,
      type: TOGGLE_TODO
    };
  }
  private getNextID() {
    return ++this.nextTodoID;
  }
}
Klasa oznaczona jest dekoratorem @Injectable, aby było możliwe jej wstrzyknięcie do klas komponentów poprzez Dependency Injection. Implementacja getNextID może być dowolna, tutaj dla prostoty id zadań to kolejne liczby naturalne. Jednocześnie w pliku todo.ts definiujemy sobie pomocniczą klasę oznaczającą nowe zadanie na liście:
export class Todo {  
  id:number;
  title:string;
  complete:boolean;
}
Reducer
Następnym krokiem pracy z reduksem jest stworzenie tzw. reducera. W tym przypadku reducer jest tylko jeden, bo aplikacja jest niezwykle prosta. Zaczynamy do zadeklarowania interfejsu dla stanu naszej aplikacji oraz zdefinowania stanu początkowego:
interface AppState {  
  todos: Array<Todo>;
}
const initialState:AppState = {  
  todos: []
};
Nasz reducer działa w ten sposób, że sprawdza którą z akcji ma obsłużyć i wywołuje odpowiednią funkcję:
export function rootReducer(state:AppState = initialState, action):AppState {  
  switch (action.type) {
    case ADD_TODO:
      return addTodo(state, action);
      break;
    case TOGGLE_TODO:
      return toggleTodo(state, action);
      break;
    default:
      return state;
  }
}
function addTodo(state:AppState, action):AppState {  
  return {
    todos: [
    ...state.todos,
    {id: action.id, title: action.title, complete: action.complete}
    ]
  };
}
function toggleTodo(state:AppState, action):AppState {  
  return {
    todos: state.todos.map(todo => {
      if (todo.id === action.id) {
        return {
          id: todo.id,
          complete: !todo.complete,
          title: todo.title
        };
      }
      return todo;
    })
  };
}
Reducer nie mutuje todos, zawsze zwraca nową tablicę.
Store
Na podstawie tak napisanego reducera tworzymy store, a następnie informujemy Angulara, że ten store jest dostępny jako zależność do wstrzyknięcia. Dodatkowo wywołujemy tutaj funkcję window.devToolsExtension() – jest to funkcja udostępniania przez wtyczkę Redux DevTools do przeglądarki Google Chrome. Wtyczka ta znacznie ułatwia pracę z Reduksem, pozwala na przykład przejrzeć wszystkie zdarzenia, które miały miejsce w aplikacji, a także dowolnie je cofać i powtarzać. Mała próbka możliwości:
Cały kod umieszczamy w pliku main.ts, w którym zwyczajowo znajduje się wywołanie funkcji bootstrap. Drugim, do tej pory pomijanym argumentem funkcji bootstrap jest tablica providers. Jej działanie jest analogiczne do własności o tej samej nazwie w komponentach.
const appStoreFactory = () => {  
  const appStore = createStore(rootReducer, undefined, window.devToolsExtension && window.devToolsExtension());
  return appStore;
};
bootstrap(AppComponent, [  
  provide('AppStore', { useFactory: appStoreFactory }),
  TodoActions 
]);
Warto zwrócić uwagę na nietypowy sposób w jaki użyty jest tutaj appStore – na potrzeby tego wpisu nie będę się zagłębiał w ten temat (ale na pewno samemu Dependency Injection w Angular 2 poświęce cały osobny artykuł!). Należy jedynie pamiętać, że w Angularze wstrzykiwać możemy albo klasy, albo dowolne wartości identyfikowane po nazwie. Tutaj appStore nie jest klasą, więc będzie identyfikowany pod nazwą AppStore.
Komponenty
Kolejnym krokiem jest stworzenie komponentów AddTodoComponent, TodoListComponent i TodoListItemComponent. Możemy do tego wykorzystać Angular CLI, jak już to omawiałem w jednym z poprzednich wpisów. Modyfikujemy AppComponent dodając do niego tablicę providers z wymienionymi komponentami oraz używamy ich w szablonie:
@Component({
  selector: 'my-app',
  directives: [AddTodoComponent, TodoListComponent],
  template: `
  <h1>To do list with Redux</h1>
  <my-add-todo></my-add-todo>
  <my-todo-list></my-todo-list>
  `
})
export class AppComponent {  
}
AddTodoComponent
Pierwszy z komponentów jest odpowiedzialny za dodawanie elementów do listy. Nic prostszego, zwykły input, ngModel oraz metoda, która tworzy akcję ADD_TODO:
@Component({
    selector: 'my-add-todo',
    template: `
    <form>
      <label>
        Nowe zadanie:
        <input type="text" [(ngModel)]="newTodoTitle" (keyup.enter)="addTodo()">
      </label>
    </form>
    `
})
export class AddTodoComponent {  
    newTodoTitle:string;
    addTodo() {
      if (!this.newTodoTitle) {
          return;
      }
      const action = this.todoActions.addTodo(this.newTodoTitle);
      this.appStore.dispatch(action);
      this.newTodoTitle = '';
    }
    constructor(@Inject('AppStore') private appStore:Store,
                private todoActions:TodoActions) {
    }
}
Zwróćmy uwagę w jaki sposób wstrzykiwany jest appStore – identyfikowany jest po nazwie i używamy do tego dekoratora @Inject('AppStore').
TodoListComponent
Drugi komponent naszej aplikacji to lista. Jej zadaniem będzie pobranie tablicy zadań oraz reagowanie na zmiany. Obie te rzeczy realizujemy poprzez wywołanie funkcji appStore.subscribe() w momencie inicjalizacji komponentu. Ważne jest jednak, aby tę subskrypcję usunąć gdy komponent jest niszczony. Posłużą do tego zdarzenia cyklu życia. Gdy już mamy tablicę zadań, wyświetlimy je w szablonie przy pomocy *ngFor i komponentu TodoListItemComponent, do którego przekażemy obiekty z zadaniami:
@Component({
  selector: 'my-todo-list',
  directives: [TodoListItemComponent],
  template: `
  <my-todo-list-item *ngFor="let todo of todos" [todo]="todo"></my-todo-list-item>
  `
})
export class TodoListComponent implements OnInit, OnDestroy {  
  todos:Array<Todo>;
  private unsubscribe:Function;
  ngOnInit() {
    this.unsubscribe = this.appStore.subscribe(() => {
      const state = this.appStore.getState();
      this.todos = state.todos;
    });
  }
  ngOnDestroy() {
    this.unsubscribe();
  }
  constructor(@Inject('AppStore') private appStore:Store) {
  }
}
TodoListItemComponent
To już chyba tylko formalność. Ten komponent reprezentuje element na liście zadań i ma jeden atrybut wejściowy @Input() todo, a po kliknięciu na checkbox wysyłana jest odpowiednia akcja:
@Component({
  selector: 'my-todo-list-item',
  template: `
  <label>
    <input type="checkbox" (change)="onTodoClick()" [checked]="todo.complete">
    {{ todo.title }}
  </label>
  `
})
export class TodoListItemComponent {  
  @Input() todo:Todo;
  onTodoClick() {
    const action = this.todoActions.toggleTodo(this.todo.id);
    this.appStore.dispatch(action);
  }
  constructor(@Inject('AppStore') private appStore:Store,
              private todoActions:TodoActions) {
  }
}
Demo
Aplikacja jest gotowa. Zaimplementowaliśmy bardzo prostą listę zadań z użyciem frameworka Angular 2 oraz biblioteki Redux. Efekt jest widoczny poniżej. Tak prosty przykład być może nie oddaje jeszcze pełni zalet wynikających z wykorzystania Reduksa, jednak gdy aplikacja będzie rosła na pewno docenimy możliwości, które daje Redux oraz łatwość z jaką możemy rozwijać nasze komponenty bez konieczności czynienia drastycznych zmian w innych częściach aplikacji. Zachęcam do komentowania :)