Redux: Using async middlewares vs dispatching acti

2019-02-10 19:28发布

问题:

I am trying to integrate Redux into my React project. Currently I'm not using any Flux framework.

My app gets some data from the API and displays it in a pretty way, like so:

componentDidMount() {
  getData();
}

getData() {
  const self = this;

  ajax({
    url: apiUrl,
  })
  .success(function(data) {
    self.setState({
      data: data,
    });
  })
  .error(function() {
    throw new Error('Server response failed.');
  });
}

In reading about Redux, I've settled on two possible approaches that I could use for handling storing my success data in the store:

  • Use async middlewares, or
  • Dispatching action ADD_DATA from the success callback of the ajax function

But I'm unsure which is the better approach.

Dispatching action in callback sounds easy to implement and understand, while async middlewares are harder to explain to people who are not used to working with a functional language.

回答1:

I personally prefer using custom middleware to accomplish this. It makes the actions a little easier to follow and has less boilerplate IMO.

I've set up my middleware to look for an object returned from a action that matches a certain signature. If this object schema is found, it handles it specially.

For example, I use an action that looks like this:

export function fetchData() {
  return {
    types: [ FETCH_DATA, FETCH_DATA_SUCCESS, FETCH_DATA_FAILURE ],
    promise: api => api('foo/bar')
  }
}

My custom middleware sees that the object has a types array and a promise function and handles it specially. Here's what it looks like:

import 'whatwg-fetch';

function isRequest({ promise }) {
  return promise && typeof promise === 'function';
}

function checkStatus(response) {
  if (response.status >= 200 && response.status < 300) {
    return response;
  } else {
    const error = new Error(response.statusText || response.status);
    error.response = response.json();
    throw error;
  }
}

function parseJSON(response) {
  return response.json();
}

function makeRequest(urlBase, { promise, types, ...rest }, next) {
  const [ REQUEST, SUCCESS, FAILURE ] = types;

  // Dispatch your request action so UI can showing loading indicator
  next({ ...rest, type: REQUEST });

  const api = (url, params = {}) => {
    // fetch by default doesn't include the same-origin header.  Add this by default.
    params.credentials = 'same-origin';
    params.method = params.method || 'get';
    params.headers = params.headers || {};
    params.headers['Content-Type'] = 'application/json';
    params.headers['Access-Control-Allow-Origin'] = '*';

    return fetch(urlBase + url, params)
      .then(checkStatus)
      .then(parseJSON)
      .then(data => {
        // Dispatch your success action
        next({ ...rest, payload: data, type: SUCCESS });
      })
      .catch(error => {
        // Dispatch your failure action
        next({ ...rest, error, type: FAILURE });
      });
  };

  // Because I'm using promise as a function, I create my own simple wrapper
  // around whatwg-fetch. Note in the action example above, I supply the url
  // and optionally the params and feed them directly into fetch.

  // The other benefit for this approach is that in my action above, I can do 
  // var result = action.promise(api => api('foo/bar'))
  // result.then(() => { /* something happened */ })
  // This allows me to be notified in my action when a result comes back.
  return promise(api);
}

// When setting up my apiMiddleware, I pass a base url for the service I am
// using. Then my actions can just pass the route and I append it to the path
export default function apiMiddleware(urlBase) {
  return function() {
    return next => action => isRequest(action) ? makeRequest(urlBase, action, next) : next(action);
  };
}

I personally like this approach because it centralizes a lot of the logic and gives you a standard enforcement of how api actions are structured. The downside to this is that it could be a little magical to those who aren't familiar with redux. I also use thunk middleware also and both of these together solve all my needs so far.



回答2:

I use redux-thunk to make the ajax call and redux-promise to handle the promise as shown below.

  function getData() {             // This is the thunk creator
    return function (dispatch) {   // thunk function
      dispatch(requestData());     // first set the state to 'requesting'
      return dispatch(
        receiveData(               // action creator that receives promise
          webapi.getData()         // makes ajax call and return promise
        )
      );
    };
  }

Dispatching an action in callback may seem simpler for first-timers to understand, but using middleware has the following advantages:

  • thunks allow to dispatch multiple actions (as in the above example -- first set state to 'requesting', which can be used by loading indicators, etc.)
  • it allows to conditionally dispatch additional actions. E.g., fetch only if the time since last fetch exceeds a threshold
  • you can still implement all this without middleware, but using middleware helps you keep all async behaviors within action creators


回答3:

Neither approach is better because they are the same. Whether you dispatch actions in callbacks or use redux thunks, you are effectively doing the following:

function asyncActionCreator() {
  // do some async thing
  // when async thing is done, dispatch an action.
}

Personally I prefer to skip the middleware / thunks and just use callbacks. I don't really think the added overhead associated with middleware / thunks is necessary, and it's not really that difficult to write your own "async action creator" function:

var store = require('./path-to-redux-store');
var actions = require('./path-to-redux-action-creators');

function asyncAction(options) {
  $.ajax({
    url: options.url,
    method: options.method,
    success: function(response) {
      store.dispatch(options.action(response));
    }
  });
};

// Create an async action
asyncAction({
  url: '/some-route',
  method: 'GET',
  action: actions.updateData
}); 


回答4:

I think what you're really asking is whether to have your AJAX call in your action creator or your component.

If your app is small enough, it's fine to have it in your component. But as your app gets larger, you'll want to refactor. In a larger app you want your components to be as simple and predictable as possible. Having an AJAX call within your component greatly increases its complexity. Moreover, having the AJAX call within an action creator makes it more reusable.

The idiomatic Redux way is to make put all your async calls in your action creators. This makes the rest of your app more predictable. Your components are always synchronous. Your reducers are always synchronous.

The only requirement for async action creators is redux-thunk. You don't need to know the ins and outs of middleware to use redux-thunk, you just need to know how to apply it when creating your store.

The following is taken directly from the redux-thunk github page:

import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers/index';

// create a store that has redux-thunk middleware enabled
const createStoreWithMiddleware = applyMiddleware(
    thunk
)(createStore);

const store = createStoreWithMiddleware(rootReducer);

That's it. Now you can have asynchronous action creators.

Yours would look like this:

function getData() {

    const apiUrl = '/fetch-data';

    return (dispatch, getState) => {

        dispatch({
            type: 'DATA_FETCH_LOADING'
        });

        ajax({
            url: apiUrl,
        }).done((data) => {
            dispatch({
                type: 'DATA_FETCH_SUCCESS',
                data: data
            });
        }).fail(() => {
            dispatch({
                type: 'DATA_FETCH_FAIL'
            });
        });

   };

}

That's it. Whenever an action creator returns a function, thunk middleware exposes dispatch (and getState which you may not need) to permit asynchronous actions.