Uncategorized

Best tips and tricks for using NGRX

This series of blog posts which I have converted from Youtube videos. I personally have really enjoyed these videos so in order to textually document them, these are essentially some screenshots and rough notes organised. 

This particular post is based on the awesome Victor Savkin Angular Camp 2017 talk. It can be found on the link https://www.youtube.com/watch?v=vX2vG0o-rpM&feature=youtu.be

What is NgRx and why we should use it

NGRX is a programming pattern similar to Redux for React which builds up on managing your app state using a single store and make any changes to it through by sending a series of messages to the the store using Observables. NgRx has been very popular over the last year or so since it allows very big applications to manage their state deterministically and also helps in writing better tests for their states and side effects. A quick summary of the lifecycle is described below:

The component dispatches the sections which goes to the store, where it is processed by effects classes then all the necessary side effects are processed and finally, the reducer will generate a new state, which will be received by the component.

Three categories of Action

Action is what we dispatch when we want to express our intent. In NgRx, the action has two parts. First one is the type and the second part is the payload.

this.store.dispatch({

type: ADD_TODO’,

payload: data

});

Not all actions are alike, there are three categories of actions.

When we are dispatching a command, we are invoking a method. Any sort of reply is expected either a value or an error.

Commands

{

Type: ‘ADD_TODO’,

Payload: data

}

Documents

Dispatching a document tells the system that certain entity has been updated. Replies are not expected of them. This might be in more than one place and with more than one handler processing it.

{

Type: ‘TODO’,

Payload: data

}

Events

Dispatching an event tells the system that a term change has occurred. The events are also handled by more than one place and by more than one handlers, like documents. The events also do not provide the response or replies.

{

Type: ‘APP_WENT_ONLINE’,

Payload: data

}

Commands and Events

To implement an interaction, we need more than one action.

Class TodosEffect {

Constructor(private actions: Actions, private http:Http) {}

@Effect() addTodo= this.action.ofType(‘ADD_TODO’).

concatMap(todo= this.http.post(_).

Map(()= ({type: ‘TODO_ADDED’, payload: todo})));

}

Function todosReducer(todos: Todo[] = [], action: any); Todo[] {

If(action.type= ‘TODO_ADDED’) {

Return[…todos, axtion.payload];

} else{

Return todos;

}

}

Request Replay

When, dispatching a command, we expect a reply or a response. The example below, shows how.

@component({

Slector: ‘todo’,

templateUrl: ./todo.component.html’

})

Class TodoComponent{

@input() todo: Todo;

Constructor(privatestore: Store<any>) {}

Delete() {

This.store.dispatch({

Type: ‘CONFIRM_TODO_DELETION’,

Payload: {todoId: this.todo.id}

});

}

}

It has a delete command, whenever the user presses the delete button, the system has to ask the user in order to confirm the action. This action cannot be done locally, within the component because these separate confirmations may result in a URL change or any non-local effect. We need to dispatch an action and then somehow get a reply.

@component({

Slector: ‘todo’,

templateUrl: ./todo.component.html’

})

Class TodoComponent{

@input() todo: Todo;

Constructor(privatestore: Store<any>) {}

Delete() {

This.store.dispatch({

Type: ‘CONFIRM_TODO_DELETION’,

Payload: {todoId: this.todo.id}

});

}

}



@component({

Slector: ‘todo’,

templateUrl: ./todo.component.html’

})

Class TodoComponent{

@input() todo: Todo;

Constructor(privatestore: Store<any>) {}

Delete() {

This.store.dispatch({

Type: ‘CONFIRM_TODO_DELETION’,

Payload: {todoId: this.todo.id}

});

this.store.select('confirmTodoDeletionResponse').

 filter(t => t.id === this.todo.id).first().

 subscribe(r => {

 // r is either true or false

 });

 }

}

 

 

Action Deciders

Filtering Decider

It is the simplest decider of all action deciders. It allows you to select from all the actions in your application.

class TodosEffects {

 constructor(private actions: Actions, private http: Http) {}

 @Effect() addTodo = this.actions.filter(a => a.type === 'ADD_TODO').

 concatMap(todo => this.http.post().

 map(() => ({type: 'TODO_ADDED', payload: todo})));

}

Content-Based Decider

The content based decider is also common. We are taking an add to-do action and we are mapping it either to append to-do or insert to-do, which depends on the content of the payload.

Class TodosEffects {

Constructor(private actions: Actions) {}

@Effect() addTodo= this.action.typeOf(‘ADD_TODO’).map(add={

If(add.payload.addlast) {

Return({ type: ‘APPEND_TODO’, payload: add.payload});

} else {

Return({type: ‘INSERT_TODO’, payload: add.payload});

}

});

}

It uses the information from some injected entity instead of the payload. The component dispatches some actions and the context based decider decides what it should be mapped to or how the handler is supposed to process it.

Class TodosEffects {

Constructor(private actions: aCtions, private env:ENv) {}

@Effect() addTodo= this.actions.typeOf(‘ADD_TODO’).map(addTodo= {

Return({type: ‘ADD_TODO_WITH_CONFIRMATION’, payload: addTodo.payload});

} else {

Return ({type: ‘ADD_TODO_WITHOUT_CONFIRMATION’, payload: addTodo.payload});

}

});

}

Splitter

The splitter takes an action and maps it on to the array of actions. It allows us to test each and every action independently.

Class TodosEffects{

Constructor(private actions: actions) {}

@Effect() addTodo= this.action.typeOf(‘REQUEST_ADD_TODO’).flatMap(add= [

{type: ‘ADD_TODO’, payload: add.payload},

{type: ‘LOG_OPERATION’, payload: {loggedActon: ‘ADD_TODO’, payload: add.payload}}

]);

}

Aggregator

Aggregator is the opposite of splitter, it takes an array of actions and it mends this array in this single action.

classTodosEffects{

constructor(private actions: Actions) {}

@Effect() aggregator= this.actions.typeOf(ADD_TODO).flatMap(a = zip(

This.actions.filter(t=t..type=’TODO_ADDED’ && t.payload.id=a.payload.id).first(),

This.actions.filter(t=t..type=’LOGGED’ && t.payload.id=a.payload.id).first()

)

).map(pair= ({

Type: ‘ADD_TODO_COMPLETED’

Payload: {id: pair [theta].payload.id, log: pair[1].payload}

}));

}

Action Transformers

The purpose of the action transformer is to take an action and transform it in to a different action. There are two most common action transformers.

Content Enricher

It takes an action and information from elsewhere and adds it in the action to emit a new one. the basic example of this phenomenon is given below.

Class TodoEffects{

Constructor(private actions: Actions, private currentUser: User) {}

@Effect() addTodo= this.actions.ofType(‘ADD_TODO’).

Map(add=({

Action: ‘ADD_TODO_BY_USER’,

Payload: {…add.payload, user: this.currentUser}

}));

}

Normaliser and Canonical Actions

It takes multiple actions, that are somewhat similar and it transforms them in to an action of the same type.

Class TodoEffects{

Constructor(private actions: Actions) {}

@Effect() insertTodo= this.actions.ofType(‘INSERT_TODO’).

Map(insert= ({

Action:’ADD_TODO’,

Payload:{…insert.payoad, append:false}

}));

@Effect() appendTodo= this.actions.ofType(‘APPEND_TODO’).

Map(insert=({

Action: ‘ADD_TODO’,

Payload: {…insert.payload, append: true}

}));

}

The common building blocks consist of 5 deciders, 2 transformers, a reducer and side effects.

Composing building blocks

We are taking an observable of all actions that are happening in our application. Then, we are filtering it. That is how generally the actions are handled in the beginning. Then it is split into two separate actions. The top one is decided by the content base decider (how to handle it), the bottom one is simpler. Then, we execute all needed side effects, we aggregate the results and the aggregation is sent to the reducer to create a new state.

Resources:


Leave a Reply

Your email address will not be published. Required fields are marked *