Add ngrx/store

Dataflow

ngrx/store is RxJS powered state management for Angular 2 apps, inspired by Redux. It helps us manage all states in one place (Store).

Without ngrx/store, the Component updates the View directly.

With ngrx/store, the dataflow looks like this. The Component first dispatch an Action. When the Reducer gets the Action, it will update the states in the Store.

The Store has been injected to the Component, so the View will update based on the store state change.

Add ngrx/store

First install

npm install @ngrx/core --save
npm install @ngrx/store --save

We will take profile as example to explain how to use ngrx/store.

We suppose that the name in profile is saved in client side only, which means no interaction with the database in the server.

src/app/profile/actions/profile.actions.ts

export class ProfileActions {
  static PROFILE_UPDATE_PROFILE = '[Profile] Update Profile';
}

This is our reducer:

src/app/profile/reducers/profile.reducer.ts

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

import { ProfileActions } from '../actions/profile.actions';

export interface ProfileState {
  _id: string,
  name: string
}

const initialState: ProfileState = {
  _id: '0',
  name: 'Jack'
};

export const profileReducer: ActionReducer<ProfileState> = (state = initialState, action: Action) => {
  switch (action.type) {
    case ProfileActions.PROFILE_UPDATE_PROFILE: {
      return Object.assign({}, state, { name: action.payload });
    }

    default: {
      return state;
    }
  }
};

When we get action PROFILE_UPDATE_PROFILE, we will set name to the value of action.payload.

Note here we are using Object.assign({}, state, { name: action.payload });, basicly it combine the existing object state and object { name: action.payload } to a new object. This makes sure the state immutable (not change the existing state).


src/app/shared/models/state.model.ts

import { ProfileState } from '../../profile/reducers/profile.reducer';

export interface State {
  profile: ProfileState
}

State is the root of the state tree. Right now it only includes ProfileState, we will add ChatState later.


src/app/profile/components/profile.component.ts

import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';

import { State } from '../../shared/models/state.model';
import { ProfileService } from '../services/profile.service';
import { ProfileState } from '../reducers/profile.reducer';
import { ProfileActions } from '../actions/profile.actions';

@Component({
  selector: 'my-profile',
  template: `
    <my-info
     [name]="(profileModel$ | async)?.name"
     (updateProfile)="onUpdateProfile($event)">
    </my-info>
  `
})
export class ProfileComponent implements OnInit {
  profileModel$: Observable<ProfileState>;

  constructor(
    private store: Store<State>
  ) {}

  ngOnInit() {
    this.profileModel$ = this.store.select<ProfileState>('profile');
  }

  private onUpdateProfile(name: string) {
    this.store.dispatch({ type: ProfileActions.PROFILE_UPDATE_PROFILE, payload: name });
    window.history.back();
  }
}

this.store.dispatch({ type: ProfileActions.PROFILE_UPDATE_PROFILE, payload: name }); means that we dispatch an Action PROFILE_UPDATE_PROFILE, and the payload is the value of name. Remember the action.payload in the reducer? It will get the value which we give here.

State is the root of the state tree. In this Component, we only need the ProfileState. So we use this.store.select<ProfileState>('profile'); to select this piece of the state which is ProfileState.

In (profileModel$ | async)?.name, profileModel$ is an Observable. async is AsyncPipe from Angular 2. It accepts a Promise or Observable as input and subscribes to the input automatically, with emitted values being displayed within your view.

So when the name in the state tree updates, (profileModel$ | async)?.name will also update.


We need add profileReducer in the @NgModule.

src/app/app.module.ts

// ...
import { StoreModule } from '@ngrx/store';
import { profileReducer } from './profile/reducers/profile.reducer';

@NgModule({
  imports: [
    // ...
    StoreModule.provideStore({
      profile: profileReducer
    })
  ],
  // ...

One way to import RxJS is using import 'rxjs' which will import all operators. Another way is only import the operators we need.

These operators below are commonly used and we will use them in our app later, so we import them here at once just for convenience.

src/app/shared/lib/rxjs-operators.ts

import '@ngrx/core/add/operator/select';  // for this.store.select

// in later chapters, we will use these operators
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/throw';

import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/switchMap';

Don't forget to import the rxjs-operators file in

src/main.ts

import './app/shared/lib/rxjs-operators';
// ...

Your profile (name) now is stored in the state. And you can update your name in the store. However, it won't update the name in the channel header. You should have the ability doing yourself now.

src/app/chat/components/chat.component.ts

// ...
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { ProfileState } from '../../profile/reducers/profile.reducers';


@Component({
  // ...
  template: `
      <my-channel-header
        [name]="(profileModel$ | async)?.name"
        (goProfile)="onGoProfile()">
      </my-channel-header>
      <!-- ... -->
  `,
})
export class ChatComponent implements OnInit, OnDestroy {
  profileModel$: Observable<ProfileState>;
  // ...

  constructor(
    // ...
    private store: Store<State>
  ) {}

  ngOnInit() {
    this.profileModel$ = this.store.select<ProfileState>('profile');
    // ...
  }

Learn more

If you want to have deep understanding of ngrx/store, we recommend you read Comprehensive Introduction to @ngrx/store.


Run the live example for this part.

results matching ""

    No results matching ""