Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ngrx one request in flight at a time

Tags:

angular

rxjs

ngrx

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.

like image 658
bygrace Avatar asked Mar 07 '23 10:03

bygrace


1 Answers

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.

like image 129
cartant Avatar answered Mar 20 '23 08:03

cartant