React Router 6 Deferred Fetch

deferred data

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

React Router 6 introduced a “deferred” API that allows you to “wait” for critical data and “postpone” optional data while calling your loader.

The best part is that you can switch between one mode or the other just by adding or removing the await keyword from the promise that resolves the data. Ryan Florence Gave an excellent explanation of this mechanism in his speech “When to Fetch” (seriously, it’s a wonderful thing, if you haven’t seen it, bookmark it and check it out after reading this post!)

I took a look at the documentation to find the deferred demo app and all its possibilities from the React Router examples folder, however, I couldn’t find an example with fetch So I decided to try it out and play with it, here’s what I got.

using the defer Fetch. with

I built a mock server with MSW to simulate a fetch delay as well as display the same text from the example.

Here’s my first naive attempt:

// 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: await 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!')
enter fullscreen mode

exit fullscreen mode

So what are we doing here?

First, returning a “naked fetch” from a normal loader works because that’s what the loader expects and React Router will open the response for you, however, defer Accept values either Promises which resolve to the values, so to get the values ​​we have to unwrap the received response manually.

Fetch is great! However, you’ll have to do some extra work, including throwing errors and opening promises as we’ve done above. Use the platform Yay! I

Here is the result:

until I opened the network tab and something didn’t seem right I

2022-11-03 21 39 00 .  capture on

The first two requests for important data which use await Keywords are happening in a waterfall, not in parallel like the rest! In fact, if you add await to all calls to the deferred object they all spring up, what gives!

No!, not at all! it turns out i forgot how does javascript work™️

picture description

waterfall is because every time you create a different awaitThis will stop execution before continuing to the next line and fetching the next one.

What we want to do to avoid waterfall is to fire all those requests at the same time and only await not real to react fetch,

“The sooner you start a fetch, the better, because the sooner it starts, the sooner it can end”

, @TkDodo

To achieve this we can declare and activate all fetch requests and then add await For important data in the object deferred later.

// 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
enter fullscreen mode

exit fullscreen mode

you can also Promise.all() But from the above example, it is easy to understand what is happening.

Here’s what the Network tab looks like now, pretty parallel green bars.

parallel fetch

Now that we’ve got the waterfall fix, let’s play around with delay and explore some interesting features of delay:

important data

critical data uses the ‘wait’ keyword, so the loader and React Router will wait until the data is ready before the first render (no loading spinner).

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

Capture 2022-11-03 and 22 35 24

If you want to fail gracefully and not destroy the whole page then remove the wait which is basically telling React Router, Hey! I don’t care if this data fails, it’s not (critical) so display a local error threshold instead. that’s right lazyError doing in the first example.

lazy plow

we are not using a await Feather lazyResolved field, although we don’t see a loading spinner at all. How is that? This is really an amazing feature of deferring, if your alternate data is fast (faster than your critical data) you will not see the spinner at all because your promise will be resolved until the critical data is finished and the first render is done. ,

The slowest is the significant data delay 500ms But, he lazyResolved only takes data 100ms so according to time critical2is resolved, lazyResolved The promise is already 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, If it is fast then it will immediately display alternate data or if it is slow then shows loading spinner.

You can play with changing the delay and increasing/decreasing the time to see if the spinners are shown. For example, if we increase the delay lazyResolved To 3500ms We will see a loading spinner.

22 41 40 on 2022-11-03 .  capture on


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

Source code for examples is available here:

Leave a Comment