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 it?
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,
let uppercaseGreeting = {
type: 'TO_UPPERCASE'
/* payload: ... */
}
Reducer:
The application state is subject to change. A reducer is a pure function that takes the current state and action being 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';
/* Following is a snippet of the constructor part of a component */
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';
/* Following is a snippet of the constructor part of a component */
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,
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' (zero). 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,
import { StoreModule } from '@ngrx/store';
import { counterReducer, greetingReducer } from './reducers';
@NgModule({
imports: [
StoreModule.forRoot({ counterReducer, greetingReducer })
]
})