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:
Thanks!
NgRx Effects provides us with a way to isolate interactions, with the aforementioned services, from the components. Within Effects, we can manage various tasks ie. communication with the API, long-running tasks, and practically every other external interaction.
@ngrx/effects provides an Angular actions$ service (which is also an Observable ) to emit every action that has been dispatched by your application in a single stream. Its ofType() method can be used to filter the one or more actions we're interesting in before adding a side-effect.
NgRx/store is a library for managing state in your Angular applications, it is a reactive state management library powered by RxJS. Similar to Redux, this library can be used to manage the flow of data throughout your application, when actions are dispatched, reducers act on them and mutate the store.
NgRx is a set of Angular libraries that uses the redux pattern and reactive extensions (RxJS) to solve state management problems in Angular. State management problems often sound like: Do I modify data in the child component or bubble an event to its container component?
NgRx Store provides reactive state management for Angular apps inspired by Redux. Unify the events in your application and derive state using RxJS. NgRx Effects gives you a framework for isolating side effects from your components by connecting observables of actions to your store.
To create Effect, NgRx provides createEffect function. sourceFunction : A function which returns an Observable . config : Optional. It is a Partial<EffectConfig> to configure effect.
NgRx is a global state management library that helps decouple the Domain and Business layers from the Rendering layer. It's fully reactive. All changes can be listened to using simple Observables, which makes complex business scenarios easier to handle. Why should I use NgRx?
@ngrx/effects is an opinionated way of managing side effects within your @ngrx based application. Some well-known redux libraries for managing side effects are redux-thunk , redux-saga, and redux-observable.
The topic is too wide. It will be like a tutorial. I will give it a try anyway. In a normal case, you will have an action, reducer and a store. Actions are dispatched by the store, which is subscribed to by the reducer. Then the reducer acts on the action, and forms a new state. In examples, all states are at the frontend, but in a real app, it needs to call backend DB or MQ, etc, these calls have side effects. The framework used to factor out these effects into a common place.
Let's say you save a Person Record to your database, action: Action = {type: SAVE_PERSON, payload: person}
. Normally your component won't directly call this.store.dispatch( {type: SAVE_PERSON, payload: person} )
to have the reducer call the 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 when adding real life error handling. To avoid this, it will be nice to just call this.store.dispatch( {type: SAVE_PERSON, payload: person} )
from your component.
That is what the effects library is for. It acts like a JEE servlet filter in front of reducer. It matches the ACTION type (filter can match urls in java world) and then acts on it, and finally returns a different action, or no action, or multiple actions. Then the reducer responds to the output actions of effects.
To continue the previous example, with the effects library:
@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 the UI has auto saving plus manually saving, to avoid unnecessary saves, UI auto save part can just be triggered by timer and manual part can be triggered by user click. Both would dispatch a SAVE_CLIENT action. The effects interceptor 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 => Observable.of( {type: SAVE_PERSON_ERR, payload: err}) )
only works once if there is an error. The stream is dead after an error is thrown because the catch tries on outer stream. The call should be
...switchMap( person => this.personService.save(person) .map( res => {type: SAVE_PERSON_OK, payload: res.json} ) .catch( e => Observable.of( {type: SAVE_PERSON_ERR, payload: err}) ) )
Or another way: change all ServiceClass services methods 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: any): ServiceResult { let ret = new ServiceResult(); ret.error = JSON.stringify(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.err ); } } @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}; } ... ... }
One correction to my previous comment: Effect-Class and Reducer-Class, if you have both Effect-class and Reducer-class react to the same action type, Reducer-class will react first, and then Effect-class. Here is an example: One component has a button, once clicked, called: this.store.dispatch(this.clientActions.effectChain(1));
which will be handled by effectChainReducer
, and then ClientEffects.chainEffects$
, which increases the payload from 1 to 2; wait for 500 ms to emit another action: this.clientActions.effectChain(2)
, after handled by effectChainReducer
with payload=2 and then ClientEffects.chainEffects$
, which increases to 3 from 2, emit this.clientActions.effectChain(3)
, ..., until it is greater than 10, ClientEffects.chainEffects$
emits this.clientActions.endEffectChain()
, which changes the store state to 1000 via effectChainReducer
, finally stops here.
export interface AppState { ... ... chainLevel: number; } // In NgModule decorator @NgModule({ imports: [..., StoreModule.provideStore({ ... ... chainLevel: effectChainReducer }, ...], ... providers: [... runEffects(ClientEffects) ], ... }) export class AppModule {} export class ClientActions { ... ... static EFFECT_CHAIN = "Chain Effect"; effectChain(idx: number): Action { return { type: ClientActions.EFFECT_CHAIN, payload: idx }; } static END_EFFECT_CHAIN = "End Chain Effect"; endEffectChain(): Action { return { type: ClientActions.END_EFFECT_CHAIN, }; } static RESET_EFFECT_CHAIN = "Reset Chain Effect"; resetEffectChain(idx: number = 0): Action { return { type: ClientActions.RESET_EFFECT_CHAIN, payload: idx }; } export class ClientEffects { ... ... @Effect() chainEffects$ = this.update$.whenAction(ClientActions.EFFECT_CHAIN) .map<number>(toPayload) .map(l => { console.log(`effect chain are at level: ${l}`) return l + 1; }) .delay(500) .map(l => { if (l > 10) { return this.clientActions.endEffectChain(); } else { return this.clientActions.effectChain(l); } }); } // client-reducer.ts file export const effectChainReducer = (state: any = 0, {type, payload}) => { switch (type) { case ClientActions.EFFECT_CHAIN: console.log("reducer chain are at level: " + payload); return payload; case ClientActions.RESET_EFFECT_CHAIN: console.log("reset chain level to: " + payload); return payload; case ClientActions.END_EFFECT_CHAIN: return 1000; default: return state; } }
If you run the above code, the output should look like:
client-reducer.ts:51 reducer chain are at level: 1
client-effects.ts:72 effect chain are at level: 1
client-reducer.ts:51 reducer chain are at level: 2
client-effects.ts:72 effect chain are at level: 2
client-reducer.ts:51 reducer chain are at level: 3
client-effects.ts:72 effect chain are at level: 3
... ...
client-reducer.ts:51 reducer chain are at level: 10
client-effects.ts:72 effect chain are at level: 10
It indicates reducer runs first before effects, Effect-Class is a post-interceptor, not pre-interceptor. See flow diagram:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With