Stav Alfi Stav Alfi - 2 months ago 57
TypeScript Question

What is the purpose of ngrx/effects library?

I have failed to find any usefull information about this library or what is the purpose of it.
It seems like ngrx/effects explain this library to developers who already know this concept and gives a bigginer example on how to code.

My questions:


  1. What are sources of actions?

  2. What is the purpose of ngrx/effects library;what is the downside of only using ngrx/store?

  3. When it is recommended to be used?

  4. Does it support angular rc 5+? How do we configure it in rc 5+?



Thanks!

Answer

Topic is too wide. It will be like a tutorial. I will give it a try. In normal case, you will have action, reducer and store. Actions are dispatched by store, which is subscribed by reducer, and then reducer acts on the action, forms a new state. For tutorial, all states are at the frontend, but in real app, it needs to call backend DB or MQ, etc, these calls have side effects, to factor out these effects into a common place, that is the framework used for.

Let's say to save a Person Record to database, action: Action = {type: SAVE_PERSON, payload: person}. Normally component won't directly call this.store.dispatch( {type: SAVE_PERSON, payload: person} ) to let reducer calling HTTP service, instead it will call this.personService.save(person).subscribe( res => this.store.dispatch({type: SAVE_PERSON_OK, payload: res.json}) ). The component logic will get more complicated if thinking about error handle in real life. To avoid this, it will be nice if just calling this.store.dispatch( {type: SAVE_PERSON, payload: person} ) from component.

That is the effects library comes for. It acts like JEE servlet filter in front of reducer. It matches the ACTION type (filter can match urls in java world) and then act on it, and finally return a different action, or no action or multiple actions, and then reducer responses to the output actions of effects.

Continue on previous example, in effects library way:

@Effects() savePerson$ = this.stateUpdates$.whenAction(SAVE_PERSON).map<Person>(toPayload).switchMap( person => this.personService.save(person) ).map( res => {type: SAVE_PERSON_OK, payload: res.json} ).catch( e => {type: SAVE_PERSON_ERR, payload: err} )

The weave logic is centralised into all Effects and Reducers classes. It can easily grow more complicated, and at the same time this design makes other parts much simpler and more re-usable.

For example if UI has an auto saving plus manually saving, to avoid unnecessary saves, UI auto save part can just be triggered by timer and manually part is triggered by user click, both dispatches SAVE_CLIENT action, in the effects interceptor, it can be:

@Effects() savePerson$ = this.stateUpdates$.whenAction(SAVE_PERSON).debounce(300).map<Person>(toPayload)
.distinctUntilChanged(...).switchMap( see above )
// at least 300 milliseconds and changed to make a save, otherwise no save

The call ...switchMap( person => this.personService.save(person) ).map( res => {type: SAVE_PERSON_OK, payload: res.json} ).catch( e => {type: SAVE_PERSON_ERR, payload: err} ) only works once if there is an error. The stream is dead after error is thrown. I cannot make it works, but I find another way: change all ServiceClasses services method to return ServiceResponse which contains error code, error message and wrapped response object from server side, i.e.

export class ServiceResult {    
    error:     string;    
    data:      any;

    hasError(): boolean {
       return error != undefined && error != null;    }

    static ok(data: any): ServiceResult {
       let ret = new ServiceResult();
       ret.data = data;
       return ret;    
    }

    static err(info: string): ServiceResult {
       let ret = new ServiceResult();
       ret.error = info;
       return ret;    
   } 
}

@Injectable()
export class PersonService {
   constructor(private http: Http) {}
   savePerson(p: Person): Observable<ServiceResult> {
       return http.post(url, JSON.stringify(p)).map(ServiceResult.ok);
              .catch( ServiceResult.ok ); 
   }
}

@Injectable()
export class PersonEffects {
  constructor(
    private update$: StateUpdates<AppState>,
    private personActions: PersonActions,
    private svc: PersonService
  ){
  }

@Effects() savePerson$ = this.stateUpdates$.whenAction(PersonActions.SAVE_PERSON).map<Person>(toPayload).switchMap( person => this.personService.save(person) ).map( res => {
   if (res.hasError()) {
       return personActions.saveErrAction(res.error);
   } else {
       return personActions.saveOkAction(res.data);
   }
});

@Injectable()
export class PersonActions {
    static SAVE_OK_ACTION = "Save OK";
    saveOkAction(p: Person): Action {
       return {type: PersonActions.SAVE_OK_ACTION,
               payload: p};
    }

    ... ...
}
Comments