The store in Redux should be synchronous and should not have any side effects.
How can you then make service calls or do anything asynchronously?
That is the question I was asking myself when I started using Redux with react-native. I needed to make calls to Google web services but in a pattern that conforms with flux/Redux way of doing things.
In Redux any asynchronous task should be part of the action itself and when it completes, the action to store should be dispatched, that is the pattern used for asynchronous tasks.
When you make a service call you also want to know whether it is in progress, has completed or if there was any error so that you can make the necessary UI changes. You want to store that information hence you will have a reducer created to maintain the state of service calls or use it to update your current state.
Redux is a very minimal library but it provides the functionality to extend it using middleware. There is a whole bunch of middleware created for redux.
Redux thunk is a middleware which is used to delay the dispatch of the action. We will use that to dispatch action before and after our service call.
Let's imagine we want to call the Marvel API to get list of comics and we have this helper function already created MarvelAPI.get_comics()
which returns a promise to the API call.
Also suppose we have the following actions for different state of calling the service
GET_COMICS_PENDING
when the request is sent.GET_COMICS_FULFILLED
when the response was successfully received.GET_COMICS_REJECTED
when there was an error.
Following is how we would call the API in our action.
export function get_comics() {
const action_type = "GET_COMICS";
return dispatch => {
// Dispatch the action for telling our reducer
// that the API call is in progress
dispatch({type: `${action_type}_PENDING`});
MarvelAPI.get_comics()
.then(resp => {
// Dispatch the success action with the payload
dispatch({
type: `${action_type}_FULFILLED`,
payload: resp.data.results
});
})
.catch(err => {
// Dispatch the error action with error information
dispatch({
type: `${action_type}_REJECTED`,
error: err
});
});
};
}
Here when we call the get_comics
action we first dispatch the GET_COMICS_PENDING
action, then we call the Marvel API and as its continuation we dispatch the action GET_COMICS_FULFILLED
with the payload in case of success otherwise we dispatch the GET_COMICS_REJECTED
with the error.
Following is how our reducer will handle all the actions
const initial_state = {
comics: [],
inProgress: false,
error: null
};
const action_type = "GET_COMICS";
export function comics_reducer(state = initial_state, action) {
switch(action.type) {
case `${action_type}_PENDING`:
return {
...state,
inProgress: true,
error: false
};
case `${action_type}_FULFILLED`:
return {
...state,
comics: action.payload,
inProgress: false
};
case `${action_type}_REJECTED`:
return {
...state,
inProgress: false,
error: action.error
};
}
}
The reducer is pretty simple, we add the comics information in case it's successful otherwise in case of failure the error information is added.
This does the job but let's suppose you have multiple such API calls then writing all that boilerplate would be tedious. That is where we will use another middleware that does the job for us.
The middleware we will use is redux-promise-middleware. It uses Flux Standard Action which is a standard way to create actions. This enables different tools to rely on the action schema and build better experience for the user. To create these actions we will use redux-actions. You can always create it by hand as well but that library just makes it easier.
import { createAction } from 'redux-actions';
export const get_comics = createAction("GET_COMICS", () => {
return {
promise: MarvelAPI.get_comics()
};
});
This is all you have to do for creating the action and our existing reducer would work. The redux-promise-middleware
expects an action that looks like following
{
type: // The type
payload: {
promise: // A promise, could be a service call or some async storage call
}
}
It will then resolve that promise and also dispatch the appropriate actions i.e. GET_COMICS_PENDING
, GET_COMICS_FULFILLED
and GET_COMICS_REJECTED
that we need to handle in our reducer.
redux-promise-middleware
will suffix the action type GET_COMICS
with _PENDING
, _FULFILLED
and _REJECTED
depending on what the promise returns.
This will make depending on asynchronous tasks a lot easier. I still have to figure out how to tie up multiple service calls into one reducer.