[Fixed] How to trigger and NgRx action after 2 HTTP requests return values to the store

Issue

I have equivalent data types stored on 2 different databases.

  • In a SQL database, I am storing a massive list of all foods.
  • On a Mongo database I am storing an individual’s food data which will override the default food data provided.

Both types of data are stored in my ngrx store ApplicationState

export interface ApplicationState {
   serverFoodData: FoodData;
   userFoodData: FoodData;
   mergedFoodData: FoodData;
}

I need to keep a version of each of these so that I can save additional userFoodData overrides if a user adds them, and so that I can recompute the mergedFoodData when that happens.

As such, I need to detect when both http requests have been resolved to the store and trigger the action to compute a new mergedFoodData object. How can I do this?

Currently I am dispatch()ing and action to load each from app.component.ts

  constructor(private store: Store<ApplicationState>) {

    this.store.dispatch(new LoadFoodsAction());
    this.store.dispatch(new LoadUserDataAction(1)); // 1 = temporary userID
    // this.foodStates$ = store.select(foodStatesSelector);
    // this.foodStates$.subscribe(foodStates => this.currentFoodStates = foodStates);
    // this.store.dispatch(new BuildMergedFoodDataAction(this.currentFoodStates))
  }

As you can see I have tried to dispatch an action to merge the 2 FoodData objects, but it is dispatch()ing with unfulfilled values because the http call has not yet responded.

actions.ts

// SQL FOODS
export const FOODS_LOADED_ACTION = 'FOODS_LOADED_ACTION';
export const LOAD_FOODS_ACTION = 'LOAD_FOODS_ACTION';


export class FoodsLoadedAction implements Action {
    readonly type = FOODS_LOADED_ACTION;
    constructor(public payload?: AllFoodData) { } // '?' is important
}
export class LoadFoodsAction implements Action {
    readonly type = LOAD_FOODS_ACTION;
    constructor(public payload: number) { }
}


// USER FOODS ACTIONS
export const LOAD_USER_DATA_ACTION = 'LOAD_USER_DATA_ACTION';
export const USER_DATA_LOADED_ACTION = 'USER_DATA_LOADED_ACTION';

export class UserDataLoadedAction implements Action {
    readonly type = USER_DATA_LOADED_ACTION;
    constructor(public payload?: AllUserData) { }
}
export class LoadUserDataAction implements Action {
    readonly type = LOAD_USER_DATA_ACTION;
    constructor(public payload: number) { }
}

// MERGE FOODS ACTION
export const BUILD_MERGED_FOOD_DATA_ACTION = 'BUILD_MERGED_FOOD_DATA_ACTION';

export class BuildMergedFoodDataAction implements Action {
    readonly type = BUILD_MERGED_FOOD_DATA_ACTION;
    constructor(public payload: FoodStates) { }
}

LoadFoodsEffectService.ts listens for the dispatch from app.component.ts and calls the http service

@Injectable()
export class LoadFoodsEffectService {

  @Effect() foods$: Observable<Action> = this.actions$
    .ofType(LOAD_FOODS_ACTION)
    .switchMap( () => this.foodService.loadServerFoods(1) ) // 1 = userID
    .map(allFoodData => new FoodsLoadedAction(allFoodData) );

  constructor(private actions$: Actions, private foodService: FoodService) {
  }
}

food-service.ts called from the effects service

@Injectable()
export class FoodService {

  constructor(private http: Http) { }

  loadServerFoods(userId: number): Observable<AllFoodData> {
    return this.http.get('/api/foods', commonHttpHeaders(userId)) // see proxy.config.json for the url
      .map( res => res.json() );
  }
}

serverFoodDataReducer.ts the http response is then added to the Application State

export function serverFoodData(state: ServerFoodState = INITIAL_FOOD_DATA, action: Action): ServerFoodState {
  switch (action.type) {
    case FOODS_LOADED_ACTION:
      return handleFoodsLoadedAction(state, action);
    default:
      return state;
  }
}

export function handleFoodsLoadedAction(state: ServerFoodState, action: FoodsLoadedAction): ServerFoodState {
  return { foods: _.keyBy(action.payload.foods, 'id') };
}

Again, how can I trigger a new action, once both of the http calls have been reduced into the store?

Solution

In this case, you will need an action that triggers both of the requests to the server, and then triggers the 3 actions. I think something like this should work for you:

@Effect()
food$ = this.actions$
.ofType('LoadFood') // This one is to trigger BOTH foods, you would have 
// another action for just the SQL or Mongo data.
.switchMap(action => {
  return Observable.combineLatest(
    service.loadServerFood(),
    service.loadUserFood()
  )
}).switchMap(([serverFood, userFood]) => {
  return Observable.of(
    serverFoodLoadedAction(serverFood),
    userFoodLoadedAction(userFood),
    mergeFoodAction(serverFood, userFood)
  );
})

Leave a Reply

(*) Required, Your email will not be published