Tyler
Tyler

Reputation: 1283

Creating a Redux Observable Epic with multiple API Calls - Passing the results of the first into the second

This isn't the first time a question like this has been asked, but for some reason I'm having a real hard time wrapping my head around Observables and RxJs, so I thought I'd ask for some clarification.

I am trying to write an epic which will do the following:

  1. Receive a string
  2. Make an API call based on that string
  3. Make three more (independent) API calls based on the results of the first, and write them into state.

I have read about concatMap and forkJoin which seem like they could be useful, as concatMap should allow me to execute multiple actions consecutively, and forkJoin becuase it can be used for returning the results of the final 3 API calls as a single array.

Here's some code to try and give an idea of what I want. Apologies for the repetitive question, but I'd be grateful for an answer.

export const apiFetchEpic = (
  action$: any,
  state$: any,
  { apiClient }: IDependencies,
) =>
  action$.pipe(
    ofType(API_FETCH),
    debounce(() => timer(300)),
    withLatestFrom(state$),
    mergeMap(
      ([
        {
          payload: { config, uri },
        },
      ]: [{ payload: IApiFetchActionProps }, any]) => {
        return from(apiClient.get(uri, config)).pipe(
          mergeMap(res =>
            of(
              apiSetAction(res), // <-- I need the results of this to be passed into the next calls
              apiCompleteAction({ uri }),
            ),
          ),
          catchError(error =>
            of(apiCompleteAction({ uri }), apiErrorAction(error)),
          ),
        )
      },
    ),
  )

Upvotes: 1

Views: 2163

Answers (1)

Tyler
Tyler

Reputation: 1283

I solved it like this:

import { ofType, combineEpics } from 'redux-observable'
import {
  mergeMap,
  withLatestFrom,
  catchError,
  debounce,
  switchMap,
} from 'rxjs/operators'
import { from, of, timer, forkJoin } from 'rxjs'
import { IDependencies } from '../../configure-store'
import { apiErrorAction } from '../../reducers/errors/actions'
import {
  TOC_FETCH,
  tocSetAction,
  ITocFetchActionProps,
  tocCompleteAction,
} from '../../reducers/table-of-contents/actions'
import { defaults } from 'lodash'

// Can probably use forkJoin here, and create multiple API calls as a parallel observable
// https://github.com/patricktran/react-redux-observable-epic-forkjoin-apis/blob/master/src/epics/index.js

export const tocFetchEpic = (
  action$: any,
  state$: any,
  { apiClient }: IDependencies,
) => {
  return action$.pipe(
    ofType(TOC_FETCH),
    debounce(() => timer(300)), // anti-DDoS
    withLatestFrom(state$),
    mergeMap(
      ([
        {
          payload: { instance },
        },
      ]: [{ payload: ITocFetchActionProps }, any]) => {

        const params = { 'myParam': instance, limit: 1 }
        return from(apiClient.get('endpoint', { params })).pipe(
          mergeMap(
            (result): any => {

              const def = {
                limit: -1,
                'newParam': result.data.results[0],
              }
              const configurations = [
                {
                  uri: 'endpointOne',
                  config: defaults({}, def),
                },
                {
                  uri: 'endpointTwo',
                  config: defaults(
                    {
                      'specialParam': 'table',
                      'specialParamTwo': 'http://example.com',
                      'iso2:lang': 'en',
                    },
                    def,
                  ),
                },
                {
                  uri: 'endpointThree',
                  config: defaults(
                    {
                      'anotherSpecialParam': 'role',
                    },
                    def,
                  ),
                }, 
              ]

              // Create a bunch of observables
              const parallelObservables = configurations.map(api =>
                from(
                  apiClient.get(api.uri, { params: api.config }),
                ).pipe(
                  switchMap(response => of(response)),
                  catchError(err =>
                    of(apiErrorAction(err), tocCompleteAction({ instance })),
                  ),
                ),
              )

              // Return a forkJoin of them.
              return forkJoin(
                parallelObservables,
                (names: any, schoolNames: any, storeNames: any) => {
                  //projection
                  return { // <- The object returned here is ultimately what gets written into state
                    instance: result.data.results[0],
                    tables: names.data.results,
                    labels: [
                      ...schoolNames.data.results,
                      ...storeNames.data.results,
                    ],
                  }
                },
              ).pipe(
                switchMap(response => {
                  return of(
                    tocSetAction(response),
                    tocCompleteAction({ instance }),
                  )
                }),
              )
            },
          ),
          catchError(error =>
            of(tocCompleteAction({ instance }), apiErrorAction(error)),
          ),
        )
      },
    ),
  )
}

export default combineEpics(tocFetchEpic)


Upvotes: 1

Related Questions