Extract Fetch calls with Redux Middleware

The first thing a lot of people coming to the React side-of-things ask is, "what about async actions?" (I did too).
Since React is unopinionated on this point, you can do it however you like.

However, since I use Redux for any substantial application I develop with React, I wanted to find a good pattern to use to handle asynchronous behavior that takes advantage of the plumbing available.

I specifically use Web API Fetch in this article, but any HTTP request library could be substituted.

Async Action Creators

A pattern offered in the Redux documentation is to have some action creators perform these asynchronous actions for you. I initially used this pattern.

A pre-requisite here is to include the redux-thunk middleware. All this does is allow you to dispatch a function in addition to plain objects. See this for more details.

Your asynchronous action creator will be something like:

export function fetchData(itemId) {
	return dispatch => {
		fetch(`${API}/items/${itemId}`, {
			method: "GET",
			headers: new Headers({
				"Authorization": `Bearer ${TOKEN}`
			})
		})
		.then( response => response.json() )
		.then( json => dispatch(fetchDataSuccess(json)) )
		.catch( error => dispatch(fetchDataFailure(error)) );
	}
}

So in contrast to standard synchronous action creators which return a plain object, you return a function which takes dispatch as a parameter. Inside this function you perform your asynchronous behavior and potentially dispatch another function on completion.

With any substantial application, the number of these kinds of actions can grow wildly. Furthermore, they can be quite verbose depending on what parameters we need to pass to Fetch. Being diligent programmers that we are, we recognize this is an opportunity to eliminate redundancy by extracting these Fetch calls elsewhere.

You could easily create a module an just require it in every action file, but that approach also has some degree of repetition. Fortunately, Redux's middleware mechanism is designed for just this type of situation.

Fetch Middleware

Let's create a middleware that performs the Fetch if a Fetch parameter object exists in the action. We can define the properties of this object however we like. I have just included the most common Fetch parameters as well as success and failure handlers.

const fetchMiddleware = store => next => action => {
	if (!action || !action.fetchConfig) {
		return next(action)
	}

	let dispatch = store.dispatch
	let config = action.fetchConfig
	dispatch(config.init)

	const path = config.path || "/"
	const method = config.method || "GET"
	const headers = config.headers
	const body = config.body
	const successHandler = config.success
	const failureHandler = config.failure

	fetch(path, {
		method: method,
		headers: headers,
		body: JSON.stringify(body)
	})
	.then( response => response.json() )
	.then( json => successHandler(json) )
	.catch( error => failureHandler(error) )

}

export default fetchMiddleware

A middleware is defined as a function that takes the store as a parameter, returns a function which takes the next middleware as parameter, which in turn returns a function that takes an action to run. (mind bent yet?)

For a detailed walkthrough of how middleware works and a number of other use cases, see the excellent write-up here.

So we just check for the existence of fetchConfig on the action and proceed if so. If not, then we hand off to the next middleware.

Our function just takes apart the config object and sets any defaults as necessary. Then, execute the Fetch call and any handlers for Promise resolution.

If your success/failure handler is another action, you can just change the then/catch block to:

	.then( json => {
		dispatch(successHandler(json))
	})
	.catch( error => dispatch(failureHandler(error)) )

Now, we can include our middleware in the createStore method to ensure it's called as part of the action flow.

let app = combineReducers(reducers)
let store = createStore(
  app,
  applyMiddleware(thunk, fetchMiddleware)
)

Lastly, we just update the action creator to pass the fetchConfig object.

export function fetchData(itemId) {
	return {
    	type: "FETCH_ITEM",
        fetchConfig: {
			path: `${API}/items/${itemId}`,
			method: "GET",
			headers: {
				"Authorization": `Bearer ${TOKEN}`
			}
		}
	}
}