If you already know Redux and start a new Angular project you might wonder the difference between ngrx/store and angular-redux/store.

ngrx/store is an RxJS powered state management for Angular applications (inspired by Redux, but do not use it) and angular-redux/store is just bindings around the Redux API (uses Redux).

The choice between those two is really up to you, they both support Ahead of Time compilation and Redux devtools.

As it is a bit of a mind shift, this post explain the transition from Redux to ngrx/store where everything is an Observable!

Action

An action in Redux or ngrx/store is the same thing. An action describes the change to make to the store. While type is required on both Redux and ngrx/store, the Action signature is different. With Redux the Action object apart from type is really up to you but with ngrx/store you can only add an optional payload key.

Here is the ngrx/store Action signature:

export interface Action {
  type: string;
  payload?: any;
}

Reducers

Actions describe the fact that something happened, but don’t specify how the application’s state changes in response. This is the job of reducers. The reducer is a pure function that takes the previous state and an action, and returns the next state.

Once again Redux and ngrx/store uses the same concept. As reducers are pure functions, it is important not to mutate arguments (state or action) and to return a new reference of the state if something was modified.

import { ActionReducer, Action } from '@ngrx/store';
import { INCREMENT_COUNTER, DECREMENT_COUNTER } from '../actions';

const INITIAL_STATE = 0;

export const counterReducer: ActionReducer<number> = (state: number = INITIAL_STATE, action: Action) => {
  switch (action.type) {
    case INCREMENT_COUNTER:
      return state + 1;
    case DECREMENT_COUNTER:
      return state - 1;
    default:
      return state;
  }
}

Creating the store

Redux and ngrx/store both provide the combineReducers utility to implement a common state shape, which is a plain Javascript object containing “slices” of domain-specific data at each top-level key.

For instance if you want your state to have a list of users and the language of the client you could do something like that shape:

{
  "users": [],
  "language": ""
}

To create such a shape, you can use combineReducers and associate a key with a reducer:

import { StoreModule, combineReducers } from '@ngrx/store';
import { usersReducer } from './reducers/users';
import { languageReducer } from './reducers/language';

export function reducer(state: any, action: any) {
  return combineReducers({
    users: usersReducer,
    language: languageReducer,
  })(state, action);
}

@NgModule({
  imports: [
    BrowserModule,
    StoreModule.provideStore(reducer),
  ]
})
export class AppModule {}

Store API

GetState

With Redux, getState is a simple function that returns the current state tree of you application. The equivalent when using ngrx/store is to use the RxJS take operator. take emits only the first n items emitted by an Observable, so take(1) will return the first value only.

Getting the entire state:

let currentState = {};
store
  .select(state => state) // select the entire state
  .take(1)
  .subscribe(state => currentState = state)
console.log(currentState) // entire state

Getting a portion of the state:

let usersList = {};
store
  .select(state => state.users) // select a portion of the state
  .take(1)
  .subscribe(users => usersList = users)
console.log(usersList) // users list

or

let usersList = {};
store
  .select('users') // select a portion of the state
  .take(1)
  .subscribe(users => usersList = users)
console.log(usersList) // users list

Dispatch

Redux and ngrx/store have the same concept of dispatch. dispatch is the only way to trigger a state change by dispatching an action.

import { Component } from '@angular/core';

import { REMOVE_USER } from '../..actions';
import { AppState, IUser } from '../../reducers';

@Component({
  selector: 'users',
  templateUrl: 'users.html'
})
export class UsersComponent {
  constructor(
    private store: Store<AppState>,
  ) {}

  removeUser(user: IUser) {
    this.store.dispatch({ type: REMOVE_USER, payload: user });
  }
}

Subscribe

Redux store.subscribe takes one argument, the callback to be invoked any time an action has been dispatched. This is how with Redux you are aware of changes in the store. With ngrx/store it is really easy, you just need to subscribe to the Observable that select return.

store
  .select(state => state) // select the entire state
  .subscribe(state => {
    // called when something has changed
  })

Action creators

Action creators are function that return an Action. Both Redux and ngrx/store can leverage this pattern to create actions.

import { Action } from '@ngrx/store';

export const LOGIN = 'LOGIN';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILED = 'LOGIN_FAILED';

export const login = (): Action => ({
    type: LOGIN
});

export const loginSuccess = (token, user): Action => ({
    type: LOGIN_SUCCESS,
    payload: {
        token,
        user
    }
});

export const loginFailed = (): Action => ({
    type: LOGIN_FAILED
});

Once you have declared all you action creators, you can easily call them within your components:

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Http } from '@angular/http';

import { login, loginSuccess, loginFailed } from './../actions';

@Component({ ... })
export class LoginComponent {
  constructor(
    public store: Store<AppState>,
    public http: Http,    
  ) { }

  login(credentials = {}) {
    this.store.dispatch(login());
    return this.http.post('authentication', credentials)
      .toPromise()
      .then(r => r.json())
      .then((r) => this.store.dispatch(loginSuccess(r.access_token, r.user)))
      .catch((e) => this.store.dispatch(loginFailed()));
  }
}

A cleaner way to do this is to use ngrx/effects.


Derived Data

To deal with derived data, Redux recommends to use Reselect. With ngrx/store you can use RxJS CombineLatest operator.

Let’s imagine that the following object is your application state:

{
  "users": {
    "293580923": { "username": "shprink"},
    "423948745": { "username": "byjc"},
    "435435799": { "username": "myagoo"},
    "027859645": { "username": "whoknows"},
    ...
  },
  "trendingUsers": [293580923, 435435799, ...],
  "topUsers": [423948745, 027859645]
}

To get the trendingUsers or topUsers list of users you need to merge them with the actual users objects from the users key.

CombineLatest emits an item whenever any of the source Observables emits a new item.

To get the list of trendingUsers with just CombineLatest operator, here is what you need to do:

Observable.combineLatest(
      store.select('users'),
      store.select('trendingUsers'),
      (users: IUsersState, trendingUsers: Array<number>) 
        => trendingUsers.map(id => users[id]))

you can easily displayed the list of trendingUsers with the async pipe:

import { Store } from '@ngrx/store';
import { Observable } from "rxjs/Observable";
import 'rxjs/add/operator/combineLatest';

@Component({
    template: `
      <div *ngFor="let user of (usersStream$ | async)">
        {{user.username}}
      </div>
    `
})
export class TrendingUsersComponent {
  usersStream$: Observable<Array<IUserState>>;

  constructor(
    public store: Store<AppState>
  ) {
    this.usersStream$ = Observable.combineLatest(
      store.select('users'),
      store.select('trendingUsers'),
      (users: IUsersState, trendingUsers: Array<number>) 
        => trendingUsers.map(id => users[id]))
  }
}

Redux devtools

A cool thing with ngrx/store is that you can use Redux devtools for Chrome with ngrx/store-devtools.

import { StoreDevtoolsModule } from '@ngrx/store-devtools';

export function reducer(state: any, action: any) { ...}

@NgModule({
  imports: [
    BrowserModule,
    StoreModule.provideStore(reducer),
    StoreDevtoolsModule.instrumentOnlyWithExtension(),
  ]
})
export class AppModule {}

I hope you guys are now excited about ngrx/store, do not hesitate to leave a comment and you can also follow updates on twitter @julienrenaux