2
votes

In short, I’d like set up an epic that performs a request and dispatches an action based on the request being fulfilled or rejected. Then I'd like the epic to maybe dispatch additional action(s) based on the result and the current state.

First part is straightforward

const fetchFooEpic: Epic<ActionAny, RootState> = (action$, store) =>
  action$.pipe(
    ofType<ActionAny, ReturnType<typeof actions.foo.fetch>>(actions.foo.types.FETCH_ALL),
    switchMap(action =>
      ajax({
        method: 'GET',
        url: `${path}/foo/${action.payload}`,
        headers: { Authorization: store.getState().user.token }
      }).pipe(
        map(({response}) => actions.foo.fetchFulfilled(response)),
        catchError(error => of(actions.foo.fetchRejected(error)))
      )
    )
  )

But I'm running into trouble introducing another action or empty into the mix. I think I want to use mergeMap and empty when nothing should be dispatched, but I'm getting type errors.

const fetchMissingRelations = (response: Foo[], state: RootState) => {
  const unknown: BarId[] = foo
    .map(foo => foo.barId)
    .filter(barId => !state.bar.entities[barId])
  return unknown.length
    ? actions.bar.fetch([...new Set(unknown)])
    : empty<never>()
}

const fetchFooEpic: Epic<ActionAny, RootState> = (action$, store) =>
  action$.pipe(
    ofType<ActionAny, ReturnType<typeof actions.foo.fetch>>(actions.foo.types.FETCH_ALL),
    switchMap(action =>
      ajax({
        method: 'GET',
        url: `${path}/foo/${action.payload}`,
        headers: { Authorization: store.getState().user.token }
      }).pipe(
        mergeMap(({response}) => of(
          actions.foo.fetchFulfilled(response),
          fetchMissingRelations(response, store.getState())
          // err: property 'type' is missing in {}
        )),
        catchError(error => of(actions.foo.fetchRejected(error)))
      )
    )
  )

I end up in https://github.com/redux-observable/redux-observable/issues/339, but providing the explicit never type for empty didn't work for me.

That's the question (feel free to stop here), but here's some additional context why I'm trying to do this and I'll appreciate if someone can suggest an alternative approach:

I've got couple of state slices with relational data that all comes over the network from different API endpoints. In this case it's discussions with internal and external participants.

When I fetch discussions, I'd like to immediately parse them for any references to participants that aren't in the state yet, batching them into requests to fill in the missing data (so I can display names, avatars and such on the UI). In case all the information is already available locally, I don't want to request anything.

My initial plan was to rely on connected React components that use the data to check for missing referenced entities in the lifecycle events (componentDidMount/componentWillReceiveProps) and dispatch actions to fill in the data, so the epics can stick to their own domains and not care about what else needs to be updated.

However, that is getting a little out of hand as that state is being used in many different places that all need to do the checks and dispatch actions when necessary. As much as I like keeping the state domains separate, I think having the epics that handle discussions requests to also dispatch the actions to update other stuff will make a much smaller footprint. The reasoning is that this would free up the connected components to just render or wait for data, instead of dispatching updates to fill in missing references. But I'm all ears for better solutions.

1
This probably doesn't help you but I just thought it would be really straight forward and simple with thunk and basic middlewares. Don't really know redux-observable though.timotgl

1 Answers

1
votes

empty returns an observable, so depending upon unknown.length your fetchMissingRelations function will return either (what appears to be) an action or an observable.

You should change it so that it always returns an observable:

const fetchMissingRelations = (response: Foo[], state: RootState) => {
  const unknown: BarId[] = foo
    .map(foo => foo.barId)
    .filter(barId => !state.bar.entities[barId])
  return unknown.length
    ? of(actions.bar.fetch([...new Set(unknown)]))
    : empty<never>()
}

And you should change your mergeMap to take that into account:

...
mergeMap(({response}) => concat(
  of(actions.foo.fetchFulfilled(response)),
  fetchMissingRelations(response, store.getState())
)),