The Fetch API and asynchronous Redux state
Tags: react, reactnative, redux, javascript
Published: 2019-06-05 15:33:09

If you are building a complex React application, you are likely using a back end service and an API. If you are managing state in React, you likely are using redux. Both are great choices that I would highly recommend for your React application's architecture. However, redux's out-of-the-box synchronous state manipulation is not particularly compatible with _asynchronous_ server responses, leaving many developers scratching their heads. There are [_a lot_ of states](https://medium.com/r/?url=https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/readyState) involved in an asynchronous call, but isn't an API call just a single action?

I would like to walk you through the standardized states involved in an asynchronous API call and their relationship to the redux store.

By the end of this article, you should understand how to write an asynchronous redux action that handles each state of an API call. Each API call has the same states and logic behind when those states are triggered, so in order to prevent copy-pasting the same boilerplate for each asynchronous redux action, I will also offer [an open-source package](https://www.npmjs.com/package/fetch-action-creator) that I've used almost religiously that will handle the action creation for you.

## Prerequisites 📍

To enable asynchronous actions on your redux store, you will want to apply the [redux-thunk](https://www.npmjs.com/package/redux-thunk) middleware.

For API calls, I will be using the standardized `fetch` API. If your target browser does not support the `fetch` API, I would recommend a [`fetch` polyfill](https://www.npmjs.com/package/whatwg-fetch). I also recommend using an [`AbortController` polyfill](https://www.npmjs.com/package/abortcontroller-polyfill) if you want to be able to abort your API calls, but not if you do not desire this feature. If you prefer an alternative to the `fetch` API, such as `axios` or `XMLHttpRequests`, they are absolutely capable of handling asynchronous Redux state management, but my code examples will be based on the `fetch` API.

## What is an asynchronous action? 🐌

The first step is understanding what you are creating - unlike previous action creators that returned an action object that was immediately sent to the reducers, an asynchronous action is not an object but a function that is immediately invoked. That function accepts two parameters, each of which is a function. The first is the `dispatch` function, used to dispatch an action; the second is a `getState` function, used to get the current Redux state.

```javascript
// Synchronously add an employee.
// addEmployee("Bob");
const addEmployee = (name) => ({
  type: 'ADD_EMPLOYEE',
  name
});



// Asynchronously add an employee.
// addEmployeeAsync("Bob")
const addEmployeeAsync = (name) => {

  // Since the return value of this action creator
  //   accepts dispatch as a parameter instead of
  //   returning what is to be dispatched,
  //   I may dispatch at my leisure and as many times as I want.
  return (dispatch, getState) => {
    
    // I want to immediately and synchronously add the employee.
    dispatch(addEmployee(name));
    
    // I want to asynchronously remove the employee.
    // This is a second action in a single action creator.
    setTimeout(
      () => {
        dispatch(removeEmployee(name));
      },
      0
    );

    // I want to asynchronously re-add that employee after 5 seconds.
    // This is a third action in a single action creator.
    setTimeout(
      () => {
        dispatch(addEmployee(name));
      },
      5000
    );
  };
};
```

Normally, when your action creator returns an object, that object gets passed to your reducer. Now, when your action creators return functions, the redux-thunk middleware will immediately invoke that function instead of passing it to the reducer. That function can do anything. Unlike other action creators, this function _does not_ return the action object. Using the dispatch parameter, you can dispatch action objects to the reducer. The upside of manually dispatching them instead of returning them is that you can dispatch as many actions as needed, such as one for each state in an API call, despite having only dispatched _one_ action creator.

In summation, your components dispatch one asynchronous action (in this case, `addEmployeeAsync`). That asynchronous action in turn dispatches multiple actions (`addEmployee`, `removeEmployee`, then `addEmployee` again). There is no reason to add, remove, then add again. It’s just an example of your freedom in design.

## The States of the Fetch API 🎌

Now that we know how to create an action that can dispatch multiple states over time, let’s identify and dispatch the states of a fetch request.

The first state of an API request is _requested_ (loading). The request has been dispatched, but we have not yet received a response.

The subsequent state of an API request is either _received_ (success) or _rejected_ (error) depending on the response from the server.

The final, potential state of an API request is _aborted_ (cancelled) for if you or the user terminates the request before receiving a response.

For each API endpoint required to power your application, an initial Redux state may look something like this:

```javascript
{
  "myApiData": {
    "abortController": null,
    "aborted": false,
    "error": null,
    "loading": false,
    "response": null
  }
}
```

You will want an action for each of these states, since each of the API request’s states should be reflected in your application.

```javascript
// When the API is requested,
//   this action is sent to the reducer.
// The abortController tied to the request,
//   so passed to the request action creator.
// You may store it in your redux state for future use.
const requestMyApi = abortController => ({
  type: 'REQUEST_MY_API',
  abortController
});

// When the API responds,
//   this action is sent to the reducer.
// It includes the response, which is probably
//   the entire point of this process.
const receiveMyApi = response => ({
  type: 'RECEIVE_MY_API',
  response
});

// When the API fails to respond,
//   this action is sent to the reducer.
// The provided error is included, which can
//   be used to display to users or debug.
const rejectMyApi = err => ({
  type: 'REJECT_MY_API',
  error: err
});

// When the API request has been aborted or cancelled,
//   this action is sent to the reducer.
const abortMyApi = () => ({
  type: 'ABORT_MY_API'
});
```

## The Abort Action 🙅

In order for the API request to notify the developer that it has been cancelled, it must be passed an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) at instantiation. Despite this not being the first action dispatched, it will be the first we write, because it must be written before the API request is initialized.

```javascript
let abortController = null;
let signal;

// Since AbortController is not well-supported yet, we check for its existence.
if (typeof AbortController !== 'undefined') {
  abortController = new AbortController();
  signal = abortController.signal;
  signal.addEventListener('abort', () => {
    dispatch(abortMyApi());
  });
}

```

If the browser supports it, we create an [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController), and we add a listener for the abort signal. When the abort signal event occurs, we dispatch the abort action. The `AbortController` will later be passed as a part of the request action. This allows you to store it in your redux state, giving your components and users access to manually abort an API request via the controller.

When a `ABORT_MY_API` action is received by your reducer, you can manipulate the state accordingly: It is no longer loading, there was no response, there was no error, and it was aborted. You may prefer replacing the aborted flag with an error string to simplify your logic, if that matches your use case. I would suggest against it, however, due to such logic differences as “Can the user re-request the payload if they aborted the previous one? If an error occurred during the previous one?”

## The Request Action 📞

You should use the _request_ action to enable a loading view. Consider using a loading animation or text to notify your user that _something is happening_. The feedback goes a long way in making your application feel responsive. The `REQUEST_MY_API` action will toggle the `state.myApi.loading` from false to true. Your components can now respond to this redux state accordingly. Components that depend on the response from my API can display that they are in the process of loading.

Since a request is instantiated immediately, you can dispatch that action immediately in your asynchronous action creator: `dispatch(requestMyApi(abortController))`.

Since we have told the reducer that we have requested the data, we should actually request it: `fetch(URL, { signal })`. You can adjust your fetch options as needed. The `signal` is the one created as a part of the abort handler above.

It takes more than just requesting the data, we also need to handle the response.

## The Response Action 🙌

Once the fetch Promise resolves, we can take that response, parse it accordingly (as text or JSON), and send the parsed data to the reducer, making it accessible to your components.

```javascript
fetch(URL, { signal })
  .then(response => {

    // If this payload is JSON, use this:
    return response.json();

    // If this payload is not JSON, use this:
    return response.text();
  })
  .then(data => {

    // Now that we've parsed the response,
    //   we can send it to the reducer.
    dispatch(receiveMyApi(data));
  });
```

## The Error Action ❌

The error action is even easier. Since we’re working with promises, we just `catch`!

```javascript
fetch(URL, { signal })
  .then(parseData)
  .then(receiveMyApi)
  .then(dispatch)
  .catch(err => {

    // An error occurred at some point in this Promise.
    // Pass the error to the reducer.
    dispatch(rejectMyApi(err));
  });

```

## Considerations 🤔

There is more complex error-handling involved if your API is _successfully responding_ with _error status codes_ and an error message as a part of the parsed payload. I won’t cover that case in detail here, because it does not apply to all APIs, but you can see how I handled it in the [source code of this package](https://github.com/CharlesStover/fetch-action-creator/blob/master/fetch-action-creator.ts#L136).

You also have the power of the `getState` function. You may use the current redux state to modify (or even ignore) your current fetch request. Depending on the scenario and action, sometimes I will get the current state to see if the request is already loading or has responded in the past. If it has, I just don’t fetch. The async action was clearly dispatched in error, so I can safely ignore it — I already have the data, so fetching it will provide me with no benefit.

## Can’t Most of This Be Automated? 🤖

Yes! The [`fetch-action-creator` package](https://www.npmjs.com/package/fetch-action-creator) does all of the above so that you don’t have to copy-paste this boilerplate for every API action. Every API call will do the same series of things: create an abort controller and signal, fetch the request, parse the response, check the response for error status codes, and dispatch an action for each of the four states involved in the process.

If I love anything, it’s [DRY code](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)! That’s why I use and recommend a function that will do all of these things for you. All you are left to do is provide the differences between any two given API calls: a unique identifier, the URL, and the fetch options.

Just `npm install fetch-action-creator` or `yarn add fetch-action-creator`!

## fetch-action-creator 🐶🎾

Be sure to understand the difference between an _action_ and an _action creator_. The [`fetch-action-creator` package](https://www.npmjs.com/package/fetch-action-creator) does not return an action creator. It _is_ an action creator, so it returns an asynchronous action, meaning it returns the `(dispatch, getState) => {}` function.

Your action creator will look something like this:

```javascript
import fetchActionCreator from 'fetch-action-creator';

export const fetchMyApi = () =>
  fetchActionCreator(
    'MY_API',
    'https://path.to/api',
    null // fetch options, if any
  );
```

The `'MY_API'` string is used to generate the Redux action types: `'REQUEST_MY_API'`, `'RESOLVE_MY_API'`, `'REJECT_MY_API'`, and `'ABORT_MY_API'`.

Your React components will only bind and call the `fetchMyApi` action creator, which notably has no parameters in this example.

You can use parameters to customize your fetch actions. It’s a little more work to extend your actions inline, but the payoff in extensibility is huge.

```javascript
import fetchActionCreator from 'fetch-action-creator';

// We now call fetchAddEmployee("Bob")
const fetchAddEmployee = name =>
  fetchActionCreator(
    'ADD_EMPLOYEE',
    'https://path.to/employees',

    // POST Bob
    {
      body: name,
      method: 'POST'
    },

    // For each action, merge with object { name }
    //   to add a name property containing
    //   employee's name to the action object.
    {
      onAbort: { name },
      onReject: { name },
      onRequest: { name },
      onResolve: { name }
    }
  );
```

You can check out [the documentation](https://github.com/CharlesStover/fetch-action-creator/) for advanced options on mutating the Redux actions.

The `fetch-action-creator` package is [open-source on GitHub](https://github.com/CharlesStover/fetch-action-creator/). Pull requests are welcome!

## Conclusion 🔚

If you liked this article, feel free to give it a heart or unicorn. It’s quick, it’s easy, and it’s free! If you have any questions or relevant commentary, please leave them in the comments below.

To read more of my columns, you may follow me on [LinkedIn](https://www.linkedin.com/in/charles-stover), [Medium](https://medium.com/@Charles_Stover), and [Twitter](https://twitter.com/CharlesStover), or [check out my portfolio on CharlesStover.com](https://charlesstover.com/).