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:
- A user clicks a button in a
component
- This calls a function in a
container
, which willdispatch
anaction
- At this point, a middleware function (perhaps a
saga
) is called - Within the middlware function, an asynchronous
fetch
call is made - The action needs to be turned into a request
- The resulting JSON is transformed into the data your application expects
- The middleware will
dispatch
the data, embedded in anotheraction
- This calls a
reducer
, which will update the applicationstate
with the data payload - 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.
- Your
fecthAction
function (we’ll create that below) receives anaction
, probably from your middleware - The action is used to create a
request
, using arequestCreator
function - This request is passed into
fetch
. Note: Optionally, you can intercept requests with a mockresponder
function - Fetch returns a promise which resolves to a
response
- Optionally, the
response
can be inspected (or altered / replaced) using aresponseHandler
- The
response.json()
method is called automatically for you - The
json
is fed into atransformer
to normalize your data - 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.
createFetchAction(options)
handleRequestCreatorActions(map)
— maps an action type to arequestCreator(action)
, which creates a RequesthandleResponderActions(map)
— maps an action type to aresponder(request, action)
, which creates a ResponsehandleResponseActions(map)
— maps an action type to aresponseHandler(response, action)
, which receives a Response; must return a ResponsehandleTransformerActions(map)
— maps an action type to atransformer(json, action)
, which receives response.json(); should return datahandleFatalActions(map)
— maps an action type to afatalHandler(error, action)
, which catches errors and can return data or throw an error
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.