Search code examples
reactjsreduxredux-thunk

chaining multiple async dispatch in Redux


I am trying to chain multiple actions together in the following fashion:

A. post user data to database

B. use posted data to query Elasticsearch for results

(I do A and B in parallel)

B1. with results from ES, query original database for results from two tables B2. navigate to new page and update UI

I am using thunks right now to reason about my code, but I also found this async pattern to be extremely verbose:

export function fetchRecipes(request) {
  return function(dispatch) {
    dispatch(requestRecipes(request))    
    return fetch(url)
      .then(response => response.json())
      .then(json => dispatch(receiveRecipes(request, json))
    )
  }
}

this, along with "requestRecipes" and "receiveRecipes" as other action creators seems like quite a bit just to make one async call. (a request, a receive, and a fetch function)

summary: when you're chaining 2-3 async actions whose outputs depend on each other (I need to promisify when possible), is there a more efficient means of doing so without writing 3 functions for each async call?

I figure there had to be a way. I'm pattern matching off of the Redux docs and soon became very overwhelmed with the functions I was creating

thanks a lot for the feedback!


Solution

  • You can use redux-saga instead of redux-thunk to achieve this more easily. redux-saga lets you describe your work using generators and is easier to reason about.

    The first step is to describe how you pass your data to redux without worrying about services or async stuff.

    Actions

    // actions.js
    function createRequestTypes(base) {
      return {
        REQUEST: base + "_REQUEST",
        SUCCESS: base + "_SUCCESS",
        FAILURE: base + "_FAILURE",
      }
    }
    
    // Create lifecycle types on `RECIPES`
    export const RECIPES = createRequestTypes("RECIPES")
    
    // Create related actions
    export const recipes = {
      // Notify the intent to fetch recipes
      request: request => ({type: RECIPES.REQUEST, request})
      // Send the response
      success: response => ({type: RECIPES.SUCCESS, response})
      // Send the error
      error: error => ({type: RECIPES.FAILURE, error})
    }
    

    Reducer

    // reducer.js
    import * as actions from "./actions"
    
    // This reducer handles all recipes
    export default (state = [], action) => {
      switch (action.type) {
        case actions.RECIPES.SUCCESS:
          // Replace current state
          return [...action.response]
    
        case actions.RECIPES.FAILURE:
          // Clear state on error
          return []
    
        default:
          return state
      }
    }
    

    Services

    We also need the recipes API. When using redux-saga the simplest way to declare a service is to creating a (pure) function which reads the request as argument and returns a Promise.

    // api.js
    const url = "https://YOUR_ENPOINT";
    
    export function fetchRecipes(request) {
      return fetch(url).then(response => response.json())
    }
    

    Now we need to wire actions and services. This is where redux-saga come in play.

    // saga.js
    import {call, fork, put, take} from "redux-saga/effects"
    import * as actions from "./actions"
    import * as api from "./api"
    
    function* watchFetchRecipes() {
      while (true) {
        // Wait for `RECIPES.REQUEST` actions and extract the `request` payload
        const {request} = yield take(actions.RECIPES.REQUEST)
    
        try {
          // Fetch the recipes
          const recipes = yield call(api.fetchRecipes(request))
    
          // Send a new action to notify the UI
          yield put(actions.fetchRecipes.success(recipes))
        } catch (e) {
          // Notify the UI that something went wrong
          yield put(actions.fetchRecipes.error(e))
        }
      }
    }
    
    function* rootSaga() {
      yield [
        fork(watchFetchRecipes)
      ]
    }
    

    And that's it! Whenever a component will send a RECIPES.REQUEST action, the saga will hook up and handle the async workflow.

    dispatch(recipes.request(req))
    

    What's awesome with redux-saga is that you can easily chain async effects and dispatch actions during the workflow.