React Router 6 Deferred Fetch
Tags: react, reactrouter, javascript, webdev
Published: 2022-11-04 22:39:13

## Deferred Data

Sometimes you want to retrieve some optional data without blocking the rest of the page that contains the critical data of your application. Examples of this optional data are comments on a blog post that render after the main content, recommended products on a shopping cart, recent searches, etc.

React Router 6 introduced the "[deferred](https://reactrouter.com/en/main/guides/deferred)" API that allows you to "await" for critical data and "defer" optional data when calling your loaders.

The best part is that you can switch between one mode or the other by just adding or removing the await keyword from the promise that resolves the data. [Ryan Florence](https://twitter.com/ryanflorence) gave an excellent explanation of this mechanism in his talk "[When to Fetch](https://www.youtube.com/watch?v=95B8mnhzoCM)" (seriously, it is an amazing talk, If you haven't watched, bookmark it and watch it after you have finished reading this post!)

I took a look at the deferred demo app from the [React Router examples](https://github.com/remix-run/react-router/tree/main/examples/data-router) folder and [the documentation](https://reactrouter.com/en/main/guides/deferred) to discover all its potential, however, I couldn't find an example with `fetch` so I decided to give it a go and play around with it, here is what I found.


## Using `defer` with Fetch

I created a [mock server with MSW](https://mswjs.io/) to simulate a fetch delay as well as display the same text from the example.

Here is my first naive attempt:

```js
// Don't copy paste! bad code! keep on reading...
export const loader = async () => {

    return defer({
      critical1: await fetch('/test?text=critical1&delay=250').then(res => res.json()),
      critical2: await fetch('/test?text=critical2&delay=500').then(res => res.json()),
      lazyResolved: fetch('/test?text=lazyResolved&delay=0').then(res => res.json()),
      lazy1: fetch('/test?text=lazy1&delay=1000').then(res => res.json()),
      lazy2: fetch('/test?text=lazy2&delay=1500').then(res => res.json()),
      lazy3: fetch('/test?text=lazy3&delay=2000').then(res => res.json()),
      lazyError: fetch('/test?text=lazy3&delay=2500').then(res => { 
        throw Error('Oh noo!')
      }),
    });
}
```

So what are we doing here? 

First, returning a "naked fetch" from a normal loader works because that's what loaders expect and React Router will unwrap the response for you, however, `defer` accepts **values** or **promises** that resolve to values, so to get the values we need to unwrap the fetch response manually.

Fetch is great! however, you have to do some additional work, including throwing errors and unwrapping promises as we have done above. Use the platform yay! 😅

Here is the result:

![Defer 1](https://user-images.githubusercontent.com/17012976/199835920-3ec5d9e4-0136-46ac-9622-e6fd9b247d8f.gif)

It all looked great until I opened the network tab and **something didn't look quite right** 🤔

![Kapture 2022-11-03 at 21 39 00](https://user-images.githubusercontent.com/17012976/199839685-5b713e31-e822-415c-bb68-32c27eecc82f.gif)

The first two requests for critical data that use the `await` keyword are happening in a waterfall, not in parallel like the rest! in fact, if you add await to all the calls in the defer object they all happen in a waterfall, what gives!

- Did [Ryan](https://twitter.com/ryanflorence) lie to us? 

- Is this a bug? 🐛

- Does critical data need to happen in a waterfall? 🚿

**Nope**!, of course not! it turns out that I forgot **how Javascript works**™️

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gjk71443yajb35e1p42e.jpeg)

The waterfall occurs because every time you create a separate `await`, it will pause execution before continuing to the next line and before firing the next fetch.

What we want to do to avoid the waterfall is fire all those fetch requests at the same time and only `await` for the response not the actual `fetch`.

> “The earlier you initiate a fetch, the better, because the sooner it starts, the sooner it can finish” 

— [@TkDodo](https://twitter.com/TkDodo)

To achieve this we can declare and fire all the fetch requests and then add the `await` for critical data in the defer object later.

```js

// You can copy this one if you want
export const loader = async () => {
  
// fire them all at once  
  const critical1Promise = fetch('/test?text=critical1&delay=250').then(res => res.json());
  const critical2Promise = fetch('/test?text=critical2&delay=500').then(res => res.json());
  const lazyResolvedPromise = fetch('/test?text=lazyResolved&delay=100').then(res => res.json());
  const lazy1Promise = fetch('/test?text=lazy1&delay=500').then(res => res.json());
  const lazy2Promise = fetch('/test?text=lazy2&delay=1500').then(res => res.json());
  const lazy3Promise = fetch('/test?text=lazy3&delay=2500').then(res => res.json());
  const lazyErrorPromise = fetch('/test?text=lazy3&delay=3000').then(res => { 
        throw Error('Oh noo!')
  });

// await for the response
  return defer({
      critical1: await critical1Promise,
      critical2: await critical2Promise,
      lazyResolved: lazyResolvedPromise,
      lazy1: lazy1Promise,
      lazy2: lazy2Promise,
      lazy3: lazy3Promise,
      lazyError: lazyErrorPromise
  })
}
```

_You can also do `Promise.all()` but with the above example, it is easier to understand what's going on._

Here is what the network tab looks like now, beautiful parallel green bars.

![parallel fetch](https://user-images.githubusercontent.com/17012976/199844432-2ef6e085-c141-4637-a735-ff0d92998ce0.gif)


Now that we fixed the waterfall, let's play around with the delay and explore a couple of interesting features of defer:


## Critical Data

The critical data uses the 'await' keyword so the loader and React Router will wait until the data is ready before the first render (no loading spinners 🎉).

What happens if critical data (using await) returns an error? 🤔 well, the loader will throw and bubble up to the nearest error boundary and destroy the entire page or that entire route segment.

![Kapture 2022-11-03 at 22 35 24](https://user-images.githubusercontent.com/17012976/199847444-46212db9-50f1-45ca-9fd1-d65137afa9dd.gif)

If you want to fail gracefully and don't want to destroy the entire page then remove await which is basically telling React Router, hey! I don't care if this data fails, it is not that important (critical) so display a localised error boundary instead. That's exactly what the `lazyError` is doing in the first example.


## Lazy Resolved

We are not using an `await` on the `lazyResolved` field, however we don't see a loading spinner at all. How is that? This is actually an amazing feature of defer, if your optional data is fast (faster than your critical data) then you won't see a spinner at all because your promise will be resolved by the time the critical data finishes and the first render occurs:

The slowest critical data delay is `500ms` but the `lazyResolved` data takes only `100ms` so by the time `critical2`is resolved, the `lazyResolved` promise has already been resolved and the data is immediately available.

The best thing about defer is that you don't have to choose how to fetch your data, **it will display optional data immediately if it is fast or shows a loading spinner if it is slow.**

You can play around changing the delays and increasing/reducing the time to see if the spinners are shown or not. For example, if we increase the delay for the `lazyResolved` to `3500ms` we will see a loading spinner.

![Kapture 2022-11-03 at 22 41 40](https://user-images.githubusercontent.com/17012976/199848509-f3a66bd6-37c7-452f-8691-0a6d6aaeae4e.gif)

**Conclusion**

Defer is a great API, it took me a while to understand it and make it work with fetch but it is an amazing tool to improve performance, reliability of your pages and developer experience.

The Source code for the examples is available here:

https://github.com/infoxicator/react-router-defer-fetch