Search code examples
httpangulartypescriptngrx

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


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)
      );
    })