My Notebook: Redux & NGRX

State:


So, what do you think of the following simple app? If I tell you to represent the current state of the app in form of a simple javascript object, how would you approach?

I can only think of two different ways,

let initialState = {
    counter: 0,
    greeting: 'Hi there!'
}

Or,

let initialState = {
    state: { 
      counter: 0,
      greeting: 'Hi there!'
    }
}

However, I would prefer the first one since they (counter and greeting) don't relate to each other.

If they were instead firstName and lastName, I would have extracted them in a person type.

Action:


Actions are the ways of bringing changes to the current state of the app. Practically it is also a javascript object and contains the following two fields,

  • type - A string representing the type of the action.
  • payload - Passed in data/information along with the action being taken. Depending on the use case an action may or may not have a payload.

For example, the following action

let uppercaseGreeting = {  
    type: 'TO_UPPERCASE'
    /* payload: ... */
}

Reducer:


Application state is subjected to change. A reducer is a pure function that takes the current state and an action been dispatched upon it. Depending on the action type it produces a new state and returns it. States are immutable. So, whenever we talk about making changes, remember the changes should be made in an immutable way.

const reducer = function(state = initialState, action) {
    switch(action.type) {
        case 'INCREMENT':
            return Object.assign({}, state, { counter: state.counter + 1});
         default: 
             return state;   
    }
};

The Object.assign() method is used to copy the values of all enumerable own properties - MDN

Whenever an INCREMENT action is dispatched, only the counter property of the current state object is updated. Object.assign() creates a new state object by merging the existing state with the updated state properties.

Store:


As the name suggests, a store stores an application state tree. When creating a store, it should be configured with a root reducer so that it can have a track of the ever changing application state.

In Redux, store is created using the following syntax,

import { createStore } from "redux";

const store = createStore(reducer);

And in NGRX, it is created in the following way,

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

@NgModule({
  ...
  imports: [..., StoreModule.forRoot({ reducer })]
  ...
})

For feature modules use forFeature() instead of forRoot()

Dispatching Actions


In Redux the following will dispatch an action to the store,

store.dispatch({ type: 'INCREMENT' });  

In NGRX,

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

...
constructor(private store: Store<any>) {  
    this.store.dispatch({ type: 'INCREMENT' });
}
...

Action Creators:


An action can be wrapped in function to make it portable. Following is an action creator for incrementing the counter,

export const incrementCounter = () => ({ type: 'INCREMENT' });  

Dispatching action using action creator would be something like the following,

Redux,

store.dispatch(incrementCounter());  

NGRX,

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

...
constructor(private store: Store<any>) {  
    this.store.dispatch(incrementCounter());
}
...

Get/Select State:


To get the current application state in Redux use the getState() method,

store.getState();  

In NGRX, use select to get the state associated with a reducer.

import { Store, select } from '@ngrx/store';

constructor(private store: Store<any>) {  
    this.state$ = store.pipe(select(state => state.reducer));
  }

In ngrx, the root reducer is named simply reducer.

Slicing up into smaller states:


In a large scale solution, the whole app state is sliced up into smaller states to make sure they are easily manageable. Each state has its own reducer. The following is the root reducer of our simple app,

export function reducer(  
  state = initialState,
  action
) {
  switch (action.type) {
    case 'INCREMENT':
      return Object.assign({}, state, { counter: state.counter + 1 });
    case 'DECREMENT':
      return Object.assign({}, state, { counter: state.counter - 1 });
    case 'TO_UPPER':
      return Object.assign({}, state, {
        greeting: state.greeting.toUpperCase()
      });
    case 'TO_LOWER':
      return Object.assign({}, state, {
        greeting: state.greeting.toLocaleLowerCase()
      });
    default:
      return state;
  }
}

We can slice it up into two smaller reducers like,

counterReducer

export function counterReducer(  
  state = 0,
  action
) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

greetingReducer

export function greetingReducer(  
  state = 'Hi there!',
  action
) {
  switch (action.type) {
    case 'TO_UPPER':
      return state.toUpperCase();
    case 'TO_LOWER':
      return state.toLocaleLowerCase();
    default:
      return state;
  }
}

Note that, we are dealing with individual slices of the application state. So in the counterReducer, the state becomes the counter property itself and has the initial state value of 0. Same idea goes for the greetingReducer as well.

Combining reducers


counterReducer and greetingReducer can be combined together to yield the application state. In Redux, combineReducers method can be used inside the createStore method,

import { createStore, combineReducers } from "redux";

const store = createStore(  
  combineReducers({ counterReducer, greetingReducer})
);

In NGRX, the StoreModule.forRoot() (same goes for forFeature as well) always takes an object so no need for any additional method here,

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

import { counterReducer, greetingReducer } from './reducers';

@NgModule({
  ...
  imports: [
    ...,
    StoreModule.forRoot({ counterReducer, greetingReducer })
  ],
  ...
})