Heygrady

Announcing fetch-actions

I am proud to announce the initial release of fetch-actions, a functional wrapper for fetch designed for integrating a remote API with a redux application.

When I first started working with redux I was pretty confused about how to get data from the server. All over the redux literature they make a big deal about how everything is synchronous. Of course, you can’t get data from the server without making asyncronous calls. Out of the box, redux leaves it up to the implementor to bridge the gap between your app and the server with middleware.

Working with a server means working with an API and every API is a little bit different. It’s no wonder that redux decided to steer clear of that mine field. In the past I’ve worked with kitchen sink frameworks like Angular and Ember that provide a full story about how to manage your API. Ember Data provides the most complete API interface layer but it heavily favors the JSONAPI specification, which is widely adopted in the Rails community but few other places. The react community has been toying with relay and graphql, but there isn’t yet a cohesive story for how a react-redux project should interface with an API.

If you’re serving a graphql or a REST API, your redux application will need to communicate with it using something like fetch. Fetch-actions hopes to provide a smooth interface between your redux application’s middleware and fetch.

Where does fetch-actions fit in?

Fetch-actions is designed to ease the integration of an API into your redux application. Specifically, fetch-actions provides a clean interface for your middleware to make fetch calls. Fetch-actions is called from within your middleware (steps 4 thru 6 below) to handle actions, make a fetch call (or mock a fetch call) and return transformed data.

If you are already working on a react-redux app you are very familiar with the following pattern:

  1. A user clicks a button in a component
  2. This calls a function in a container, which will dispatch an action
  3. At this point, a middleware function (perhaps a saga) is called
  4. Within the middlware function, an asynchronous fetch call is made
  5. The action needs to be turned into a request
  6. The resulting JSON is transformed into the data your application expects
  7. The middleware will dispatch the data, embedded in another action
  8. This calls a reducer, which will update the application state with the data payload
  9. Finally, the component updates with the new data

Simple enough :)

How does fetch-actions work?

In the same way that redux is concerned with managing the application state (with actions and reducers), fetch-actions is concerned with managing the fetch request lifecycle. In the sequence above, the fetch call is far more complex than it seems. One does not simply call fetch.

Without fetch-actions, you will need to find a way to generate API queries from actions, transform the results, and dispatch the data into your app’s reducers. This lifecycle can be cleanly separated into it’s functional parts instead of embedding it within your middleware.

Let’s dig into the process a little bit further to see how fetch-actions helps you manage your API calls.

  1. Your fecthAction function (we’ll create that below) receives an action, probably from your middleware
  2. The action is used to create a request, using a requestCreator function
  3. This request is passed into fetch. Note: Optionally, you can intercept requests with a mock responder function
  4. Fetch returns a promise which resolves to a response
  5. Optionally, the response can be inspected (or altered / replaced) using a responseHandler
  6. The response.json() method is called automatically for you
  7. The json is fed into a transformer to normalize your data
  8. Finally, the data is returned to your middleware

The problem fetch-actions solves

In the same way that react-redux encourages you to move state out of your components, fetch-actions encourages you to remove requests and transforms from your middleware. By moving the request building and data normalization to fetch-actions, your middleware can be greatly simplified.

Consider this saga (presume it is called by a takeEvery saga):

import { put, call } from 'redux-saga/effects'

import { requestPosts, receivePosts } from '../modules/posts/actions'
import { fetchAction } from '../utils/api'

export const fetchPostsSaga = function * (action) {
  const { payload } = action
  yield put(requestPosts(payload))
  const { data, errors } = yield call(fetchAction, action)
  yield put(receivePosts({ ...payload, data, errors }))
}

Even if you’re not terribly familiar with redux-saga, you can see the above code is fairly straight-forward. Somewhere in the app, a fetchPosts action is dispatched. In the middleware above we dispatch an action to indicate that a request has been initiated, call fetchAction to make our API request, and dispatch an action with the resulting data and / or errors. This is very similar to the thunk example in the redux manual.

Middleware is responsible for managing the asynchronous flow with regards to your application. Fetch-actions is responsible for managing your middleware’s relationship with your API. If you were implementing fetch directly within your middleware, the saga above would include at least 7 additional steps. Embedding API logic into your middleware can get messy in cases where creating the request or transforming the response is complicated.

Note: A full tutorial is outside the scope of this document. You can see more detail in the fetch-actions docs. You may like learn more about redux-saga or asynchronous middleware.

What does a fetchAction(action) function look like?

Every application will have unique API requirements, so you’ll need to create your own function for use in your app’s middleware. The simplest fetchAction requires you to specify fetch and a requestCreator; you don’t have to provide any other handlers if you don’t want to. Because most apps will need data normalization, the example below shows a transformer as well.

import { createFetchAction, handleRequestCreatorActions, handleTransformerActions } from 'fetch-actions'

import { FETCH_POSTS } from '../modules/posts/constants'
import 'fetch-everywhere'

const requestCreator = handleRequestCreatorActions({
  [FETCH_POSTS]: action => new Request(`https://www.reddit.com/r/${action.payload}.json`)
})

const transformer = handleTransformerActions({
  [FETCH_POSTS]: (json, action) => ({
    data: json.data.children.map(child => child.data),
    errors: undefined
  })
})

export const fetchAction = createFetchAction({
  fetch,
  requestCreator,
  transformer
})

Bring your own fetch

Fetch isn’t available in every environment (browser support). It’s very common for web applications to use the whatwg-fetch polyfill. If your app runs in node or react-native, you might enjoy a platform agnostic polyfill, like fetch-everywhere. If you wanted to use a library like axios or superagent, you could supply fetchAction with a completely fake fetch function.

Note: This initial version of fetch-actions is designed with fetch in mind. In specific, fetch-actions expects that the supplied fetch function will receive a Request and return a promise that resolves to a Response. If you are replacing fetch with some other AJAX library you will need to take this into account.

Below is a contrived example where you can supply whatever you want for fetch.

import { createFetchAction } from 'fetch-actions'

import requestCreator from './requestCreators'
import transformer from './transformers'

const fetch = (input, init) => {
  const data = { whatever: 'could come from anywhere you please' }
  return Promise.resolve(new Response(JSON.stringify(data))) // <-- return a promise that resolves to a response
}

export const fetchAction = createFetchAction({
  fetch,
  requestCreator,
  transformer
})

Mocking data with a responder(request, action)

If you are implementing a new API you may need to generate fake responses using mock fetch calls. Rather that replacing fetch itself, fetch-actions allows you to specify a responder, which should return valid responses. A responder function is called instead of fetch, which makes it easy to build your app against an API, even before it exists. If your responder function returns anything other than undefined, it will be used instead of fetch.

Note: In this initial version of fetch-actions, the response that your responder returns should be a valid fetch Response.

Here’s a responder that will return mock data while you are in development:

import 'fetch-everywhere'
import { createFetchAction } from 'fetch-actions'

import data from './mock/posts.js'
import requestCreator from './requestCreators'
import transformer from './transformers'

const responder = (request, action) => {
  if (process.env.NODE_ENV === 'development') {
    return new Response(JSON.stringify(data)) // <-- return a JSON response
  }
}

export const fetchAction = createFetchAction({
  fetch,
  requestCreator,
  responder,
  transformer
})

Using handleResponderActions(map)

As your application grows you may want to return different mock data depending on the action. Fetch-actions provides a handler for mapping actions to responder functions. The handleResponderActions function expects a map object as its only argument and returns a function. The returned function is identical to a responder (it accepts a request and an action as arguments and returns a response).

The big difference is that handleResponderActions will call a specific responder function depending on the action type. If no matching responder is found, undefined is returned, which will instruct fetch-actions to pass the request to fetch instead.

Below you can see a responder that is only called for the FETCH_POSTS action.

import { createFetchAction, handleResponderActions } from 'fetch-actions'
import 'fetch-everywhere'

import { FETCH_POSTS } from '../modules/posts/constants'

import data from './mock/posts.js'
import requestCreator from './requestCreators'
import transformer from './transformers'

const fetchPostsResponder = (input, init) => {
  return Promise.resolve(new Response(JSON.stringify(data)))
}

const responder = handleResponderActions({
  [FETCH_POSTS]: fetchPostsResponder
})

export const fetchAction = createFetchAction({
  fetch,
  requestCreator,
  responder,
  transformer
})

Required: requestCreator(action)

At the very least, you need to provide a function for translating an action into a valid fetch request. Because every API is different, fetch-actions has no opinion about how you create those requests, but there is a requirement that a valid fetch request is returned.

A requestCreator is a function that receives an action as its only argument and returns a valid fetch request.

import { createFetchAction } from 'fetch-actions'
import 'fetch-everywhere'

const requestCreator = action => new Request(`https://www.reddit.com/r/${action.payload}.json`)

export const fetchAction = createFetchAction({
  fetch,
  requestCreator
})

Using handleRequestCreatorActions(map)

A your app grows, you will want to create requests based on the action type. For convenience, fetch-actions provides a handler that can map actions to requestCreator functions.

The handleRequestCreatorActions function expects a map object as its only argument and returns a function. The returned function is identical to a requestCreator (it accepts an action as an argument and returns a request). The big difference is that handleRequestCreatorActions will call a specific requestCreator function depending on the action type.

Below you can see a requestCreator that calls different creators for the FETCH_POSTS and FETCH_EXAMPLES actions.

import { createFetchAction, handleRequestCreatorActions } from 'fetch-actions'

import { FETCH_EXAMPLES } from '../modules/examples/constants'
import { FETCH_POSTS } from '../modules/posts/constants'
import 'fetch-everywhere'

const fetchPostsRequestCreator = action => new Request(`https://www.reddit.com/r/${action.payload}.json`)
const fetchExamplesRequestCreator = action => new Request(`https://example.com/${action.payload}`)

const requestCreator = handleRequestCreatorActions({
  [FETCH_POSTS]: fetchPostsRequestCreator,
  [FETCH_EXAMPLES]: fetchExamplesRequestCreator
})

export const fetchAction = createFetchAction({
  fetch,
  requestCreator
})

Optional: transformer(json, action)

Because every API is different, integrating your app with an external API can be challenging. Unless you were the one who designed the API, it likely doesn’t return the exact data that your application is expecting. Data normalization (perhaps using something like normalizr) is a key step in your API integration. Fetch-actions manages this using transformer functions.

A transformer function receives a JSON object and an action as its two arguments and returns a data object in whatever format your application is expecting. The json comes from the response.json() method on a fetch response. The action is the original action that was passed to fetchAction.

Note: This initial version of fetch-actions expects a json response. If you have an XML-only API (or some other weird response), you will want to implement a responseHandler.

Note: The example below is suggesting that your transformed data look like { data, errors } but that isn’t a hard requirement. Your data can look however you’d like. You might enjoy handling all of your errors in fetch-actions and returning well-formatted error objects to your middleware as shown below.

import { createFetchAction } from 'fetch-actions'

import requestCreator from './requestCreators'
import 'fetch-everywhere'

const transformer = (json, action) => ({
  data: json.data.children.map(child => child.data),
  errors: undefined // <-- you could pass back errors if you like
})

export const fetchAction = createFetchAction({
  fetch,
  requestCreator,
  transformer
})

Using handleTransformerActions(map)

Fetch-actions also provides a handler that can map actions to transformers. The handleTransformerActions function expects a map object as its only argument and returns a function. The returned function is identical to a transformer (it accepts a json object and an action and returns data). The big difference is that handleTransformerActions will call a specific transformer function depending on the action type.

The handleTransformerActions function works similarly to handleActions from redux-actions. In general, the handler pattern used in redux-actions was a major influence on fetch-actions.

Below you can see a transformer that is called for the FETCH_POSTS action.

import { createFetchAction, handleRequestCreatorActions, handleTransformerActions } from 'fetch-actions'

import { FETCH_POSTS } from '../modules/posts/constants'

import requestCreator from './requestCreators'
import 'fetch-everywhere'

const fetchPostsTransformer = (json, action) => ({
  data: json.data.children.map(child => child.data),
  errors: undefined
})

const transformer = handleTransformerActions({
  [FETCH_POSTS]: fetchPostsTransformer
})

export const fetchAction = createFetchAction({
  fetch,
  requestCreator,
  transformer
})

Deeply nesting transformer functions

Transformer functions are modeled after reducers. The handleTransformerActions is functionally similar to handleActions (which is designed for reducers). You may find yourself in a scenario where similar objects need to be transformed in similar ways. It’s a good practice to have your transformer function deal with one small part of the object tree and leave deeper parts of the tree to other transformers.

Here’s an example of passing part of the tree to a child transformer:

import { createFetchAction, handleRequestCreatorActions, handleTransformerActions } from 'fetch-actions'

import { FETCH_POSTS } from '../modules/posts/constants'

import requestCreator from './requestCreators'
import 'fetch-everywhere'

const childTransformer = handleTransformerActions({
  [FETCH_POSTS]: (json, action) => json.data
})

const transformer = handleTransformerActions({
  [FETCH_POSTS]: (json, action) => ({
    data: json.data.children.map(child => childTransformer(child, action)),
    errors: undefined
  })
})

export const fetchAction = createFetchAction({
  fetch,
  requestCreator,
  transformer
})

More details in the docs

The full fetch-actions API provides a number of handlers for managing the fetch lifecycle. You can read about these in the documentation.

Conclusion

If you like what you see above, you should be able to start using fetch-actions in your app right away. It is published as an NPM package.

yarn add fetch-actions

Fetch-actions makes it easy to manage actions that create and transform fetch requests. You can read more about fetch-actions in the docs or contribute to the Github repository. Feel free to open any issues you run into. Currently, fetch-actions has 100% code coverage. You might like to review the tests to learn more about the internal structure.

As noted above, I was heavily inspired to create fetch-actions after using redux-actions on a few projects. Hopefully fetch-actions can make your next API integration a little more fun.


Grady Kuhnline Written by Grady Kuhnline. @heygrady | LinkedIn | Github