Context: Angular 4.x, RxJs 5.4.x, NgRx 4.1.1.
Goal: I want to avoid repeating the same api call when multiple components request the same data. I don't want to cancel in-flight requests, just not make future ones till the in-flight one is complete.
Method: In the store there is a flag named isFetching
. In my effect I check the flag to see if there is an in-flight request for already. If so then I don't make the request. The same action that triggers the effect sets the isFetching
flag to true.
Problem: The state is updated before effects are run. So isFetching
is true before the first request is sent. So no request is ever sent.
Example: Here is a simplified version of the problem:
state.ts
export interface State {
isFetching: boolean;
}
reducer.ts
export function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH':
return <State>{ isFetching: action.payload };
}
return state;
}
effect.ts
@Effect()
public fetch: Observable<Action> = this.actions.ofType('FETCH')
.withLatestFrom(this.store.select(x => x.isFetching), (_, x) => x)
.filter(x => !x)
.switchMap(action =>
this.api.getData()
.map(data => new FetchSuccess(data))
.catch(() => Observable.of(new FetchFailure()))
);
Idea #1:
I can duplicate the state by keeping a variable in the class that contains the effect. I would set it when the api request was made and use it to filter out future requests.
effect.ts
public isFetching = false;
@Effect()
public fetch: Observable<Action> = this.actions.ofType('FETCH')
.filter(() => !this.isFetching)
.switchMap(action => {
this.isFetching = true;
return this.api.getData()
.map(data => new FetchSuccess(data))
.catch(() => Observable.of(new FetchFailure()))
.do(() => this.isFetching = false);
});
Idea #2:
Don't have the 'FETCH'
action set isFetching
. Have the effect emit two actions. The first (IsFetching
) sets the state and the second (FetchSuccess | FetchFailure
) is the result. I don't like that I need two actions to do what I feel I should be able to do with one. The advantage is that if there are multiple 'FETCH'
requests there will only be one state update from the IsFetching
action.
It seems that the results of the effect are applied to state asynchronously. So sequential synchronous 'FETCH'
actions will trigger an equivalent number of requests before the flag is set in state. So don't use this solution.
effect.ts
@Effect()
public fetch: Observable<Action> = this.actions.ofType('FETCH')
.withLatestFrom(this.store.select(x => x.isFetching), (_, x) => x)
.filter(x => !x)
.switchMap(action =>
this.api.getData()
.map(data => new FetchSuccess(data))
.catch(() => Observable.of(new FetchFailure()))
.startWith(new IsFetching(true))
);
Question Is there a more elegant/standard way to accomplish my goal? Ideally I would like to avoid keeping the same state information in two places.
One option would be to use an operator that maintains its own state.
For example, you could use distinct
, which accepts a second parameter that clears the operator's internal state upon emission:
@Effect()
public fetch: Observable<Action> = this.actions
.ofType('FETCH')
.distinct(() => 'FETCH', this.actions.ofType(
'FETCH_SUCCESS',
'FETCH_FAILURE'
))
.switchMap(action => this.api.getData()
.map(data => new FetchSuccess(data))
.catch(() => Observable.of(new FetchFailure()));
);
If the key selector always returns the same value, any FETCH
actions received whilst one is being handled will be ignored.
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