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:
ADD_DATA
from the success callback of the ajax functionBut 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.
Introduction. By default, Redux's actions are dispatched synchronously, which is a problem for any non-trivial app that needs to communicate with an external API or perform side effects.
Redux thunk is the most popular middleware that allows you to call action creators, which then returns a function instead of an action object.
Redux Async Data Flow Just like with a normal action, we first need to handle a user event in the application, such as a click on a button. Then, we call dispatch() , and pass in something, whether it be a plain action object, a function, or some other value that a middleware can look for.
Redux Middleware allows you to intercept every action sent to the reducer so you can make changes to the action or cancel the action. Middleware helps you with logging, error reporting, making asynchronous requests, and a whole lot more.
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.
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:
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
});
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With