React Suspense with the Fetch API
Tags: react, reactnative, javascript, webdev
Published: 2019-06-05 21:21:09

_Dan Abramov, in response to a React developer asking why Suspense was not responding to the fetch API:_

![](https://thepracticaldev.s3.amazonaws.com/i/ck9fdk2726r4xr0iqofz.png)

From the legend Dan Abramov himself, we receive such gems as “There is [no data fetching solution compatible with React Suspense] that exists yet,” and “[React Cache] will be the first one,” and “Suspense is limited to code splitting.”

If I have one thing to tell Daniel “Abra Cadabra” Abramov, besides how impressed I am with his work, it’s this:

![](https://thepracticaldev.s3.amazonaws.com/i/n62dtg2hkf31w9aq7k56.png)

Let’s reveal the magic behind the curtain that is React Suspense. For educational purposes, I’ll cover how I created this package.

# Shut Up and Give Me the Package! 💰

If you’re just here for solutions, I don’t blame you. You can find [`fetch-suspense` on NPM](https://www.npmjs.com/package/fetch-suspense) and the most extensive documentation of your life on [the GitHub repository](https://github.com/CharlesStover/fetch-suspense).

```javascript
import useFetch from 'fetch-suspense';

const MyComponent = () => {

  // "Look! In the example! It's a fetch() request! It's a hook!"
  //   "No! It's kind of like both at the same time."
  const serverResponse = useFetch('/path/to/api', { method: 'POST' });

  // The return value is the body of the server's response.
  return <div>{serverResponse}</div>;
};
```

# How Does Suspense Work? 🔮

A lot of the new React features are built into the React library, as opposed to being external packages, due to the performance benefits of being tightly coupled to the engine that powers React, known as _React Fiber_.

Due to React Fiber’s direct integration with features such as Suspense and hooks, you cannot create a verbatim copy of Suspense in React 16.5. However, you can probably make a less performant polyfill. I’ll use some polyfill examples so that you can conceptualize what is happening with Suspense.

```javascript
class Suspense extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      error: null
    };
  }

  componentDidCatch(e) {
    this.setState({ error: e });
  }
  
  render() {
    if (this.state.error) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

/*
<Suspense fallback={<Loading />}>
  <ErrorThrower />
</Suspense>
*/
```

Here is ye olde class component: a fossil remnant of yonder days of React development. The `componentDidCatch` method is a method that fires whenever a child component throws an error. This allows you to replace uncaught JavaScript errors with a nice UI for your users or otherwise implement important logic during application errors.

What the above does is mounts Suspense. Since there is no error in the local state, the children of Suspense are mounted too. In this case, the `<ErrorThrower />` component is mounted, and it throws an error.

That error bubbles up to the Suspense instance, where the `componentDidCatch` method receives it. It handles that error by saving it to its state, causing it to re-render.

Now that it has rendered with an error in its local state, it no longer renders its children prop, nor the `<ErrorThrower />` devil-child as a result. Instead, it renders its `fallback` prop, which we have set to a nice `<Loading />` modal.

This is how Suspense works now, except instead of throwing errors, _JavaScript Promises_ are thrown. When Suspense catches a Promise, it _re-renders_, displaying the fallback prop instead of the children that previous threw a Promise. When the Promise resolves, it _re-renders again_; this time no longer displaying the `fallback` prop, and instead attempting to re-render the original children, under the presumption that the children are now ready to be rendered without throwing Promises around like they’re meaningless.

An implementation may look something like this:

```javascript
class Suspense extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      promise: null
    };
  }

  componentDidCatch(e) {
    
    // Drake meme where he says no to errors here.
    if (e instanceof Error) {
      throw e;
    }

    // Drake meme where he says yes to promises here.
    if (e instanceof Promise) {
      this.setState({
        promise: e
      }, () => {

        // When the promise finishes, go back to rendering the original children.
        e.then(() => {
          this.setState({ promise: null });
        });
      });
    }
    
    // This line isn't compatible with the Drake meme format.
    else {
      throw e;
    }
  }
  
  render() {
    if (this.state.promise) {
      return this.props.fallback;
    }
    return this.props.children;
  }
}

/*
<Suspense fallback={<Loading />}>
  <PromiseThrower />
</Suspense>
*/
```

It’s important to note here that the original children _attempted to render_ before the fallback occurred. It never succeeded.

# How Does This Apply to Fetch Hooks? 🎣

What you should have gathered by now is that the fetch hook will need to _throw Promises_. So it does. That promise is conveniently the fetch request. When Suspense receives that thrown fetch request, it falls back to rendering its `fallback` prop. When that fetch request completes, it attempts to render the component again.

There’s just one little tricky dicky problem with that — the component that threw the fetch request had only _attempted_ to render, but did not succeed. In fact, it is not a part of the `fallback` at all! It has no instance. It never mounted. It has no state (not even a React hook state); it has no component lifecycle or effects. So when it _attempts_ to render again, how does it know the response of this fetch request? Suspense isn’t passing it, and it — not being instantiated — cannot have data attached to it.

# Golly, How Do You Solve That Conundrum? 🤔

We solve it with memoization!

“Like that fancy new `React.memo` feature?”

“Yes!” (in concept)

“No!” (more literally)

It does not use `React.memo`, which memoizes React components based on their props. Instead, I use an array of infinite depth to memoize the parameters passed to fetch.

If a request comes in to fetch data that has been requested before (the second attempt to instantiate after the first attempt failed with a Promise), then it simply returns the data that eventually resolved from the first request’s Promise. If this is a fresh request, then we fetch it, cache it in the memoization array, and throw the fetch Promise. By comparing the current request to all entries in the memoization array, we know if we have dispatched this request before.

```javascript
const deepEqual = require('deep-equal');

interface FetchCache {
  fetch?: Promise<void>;
  error?: any;
  init: RequestInit | undefined;
  input: RequestInfo;
  response?: any;
}

const fetchCaches: FetchCache[] = [];

const useFetch = (input: RequestInfo, init?: RequestInit | undefined) => {
  for (const fetchCache of fetchCaches) {

    // The request hasn't changed since the last call.
    if (
      deepEqual(input, fetchCache.input) &&
      deepEqual(init, fetchCache.init)
    ) {

      // If we logged an error during this fetch request, THROW the error.
      if (Object.prototype.hasOwnProperty.call(fetchCache, 'error')) {
        throw fetchCache.error;
      }

      // If we received a response to this fetch request, RETURN it.
      if (Object.prototype.hasOwnProperty.call(fetchCache, 'response')) {
        return fetchCache.response;
      }
      
      // If we do not have a response or error, THROW the promise.
      throw fetchCache.fetch;
    }
  }

  // The request is new or has changed.
  const fetchCache: FetchCache = {
    fetch:

      // Make the fetch request.
      fetch(input, init)

        // Parse the response.
        .then(response => {

          // Support JSON.
          if (Object.prototype.hasOwnProperty.call(response.headers, 'Content-Type')) {
            return response.json();
          }

          // Not JSON.
          return response.text();
        })

        // Cache the response for when this component
        //   attempts to render again later.
        .then(response => {
          fetchCache.response = response;
        })
    
        // Cache the error for when this component
        //   attempts to render again later.
        .catch(e => {
          fetchCache.error = e;
        }),
    init,
    input
  };
  
  // Add this metadata to the memoization array.
  fetchCaches.push(fetchCache);
  
  // Throw the Promise! Suspense to the rescue!
  throw fetchCache.fetch;
};
```

# That Sounds Like a Memory Leak 💧

It can be a feature _or_ a bug!

But if you think it’s a bug in your project, you can invalidate the cache by providing a lifespan in milliseconds to the fetch request. Passing a third parameter (a number) to the `useFetch` hook will tell it to remove the metadata from the memoization array after that many milliseconds. We implement it as easily as so:

```javascript
// NEW: lifespan parameter
const useFetch = (
  input: RequestInfo,
  init?: RequestInit | undefined,
  lifespan: number = 0
) => {

  // ...

  const fetchCache: FetchCache = {
    fetch:

      // Make the fetch request.
      fetch(input, init)
        .then( /* ... */ )
        .then( /* ... */ )
        .catch( /* ... */ )

        // Invalidate the cache.
        .then(() => {

          // If the user defined a lifespan,
          if (lifespan > 0) {

            // Wait for the duration of the lifespan,
            setTimeout(
              () => {

                // Find this fetch request and kill it
                //   from the memoization array.
                const index = fetchCaches.indexOf(fetchCache);
                if(index !== -1) {
                  fetchCaches.splice(index, 1);
                }
              },
              lifespan
            );
          }
        }),
    // ...
  };
  // ...
};

// ...
```

When the fetch has completed, and we’ve updated the metadata, tick-tock. It’s important that the lifespan timer occurs after the `catch` of the Promise, because we want it to set even if an error occurred.

# Conclusion 🍬

When Dan Abramov tells you that you can’t do something, _you do it_.

If you liked this article, feel free to give it a heart or a unicorn. It’s quick, it’s easy, and it’s free! If you have any questions or relevant great advice, 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/).