tip 002

How to solve fallback flickering when using React Suspense

This is eventually going to evolve into a tips and tricks article for using Suspense in React. Specifically when paired with deferred loaders and the <Await /> component Remix.

For now though let's solve an issue I ran into that no amount of googling helped me solve.

When using deferred loaders the most common path is going to be a case where you defer a single piece of data that you pass into your Suspense handling on the frontend that looks something like this:

import { defer } from "@remix-run/node"
import { useLoaderData, Await } from "@remix-run/react"
import { Suspense } from "react"

export function loader() {
  return defer({
    deferredPromise: fetchIt()
  });
}

function SuspendedComponent() {
  const { deferredPromise } = useLoaderData()

  return (
    }>
      }>
        {promise => }
      
    
  )
}

However, as with most applications of any size you're going to eventually run into a use case where you need two or more pieces of deferred data to render a component.

That is the situation I ran into and this is how I solved it.

How to use Suspense with multiple promises

The good news? Just use what you would whenever you're dealing with multiple promises you want to run concurrently:

import { defer } from "@remix-run/node"
import { useLoaderData, Await } from "@remix-run/react"
import { Suspense } from "react"
import { suspendAll } from "suspend-concurrently"

export function loader() {
  return defer({
    deferredPromise1: fetch1(),
    deferredPromise2: fetch2(),
  });
}

function SuspendedComponent() {
  const { deferredPromise1, deferredPromise2 } = useLoaderData()

  return (
    }>
      }>
        {([promise1, promise2]) => }
      
    
  )
}

This works great, but I noticed when navigating to a new page in the Remix app the fallback state would flicker before the page transitioned to the new route.

I tried googling what was going on, and spent a couple hours debugging. Nothing. This is the fun part of development…Finally I put a help message in the Remix discord. A lot of helpful people tried to work through it with me, but a week and a half later..still no answer.

Then Javier Villanueva put a message in the thread with the same problem. Turns out there was a common thread between both of our cases.

When using Promise.all to pass multiple promises to the <Await /> component we lose our reference to the original promises. When the page transitions and React starts the process of re-rendering the <Await /> component thinks it is getting a brand new promise. Since the promises are already resolved that is why you only see a flicker of the fallback because it immediately loads the resolved state.

Okay, well…how do we fix that? If you have been around the block in React this is an age old problem and you have already realized what the fix is. We need to preserve the reference to the new promise we have created.

Thankfully, React already gives us a hook that can be used for this exact use case. With useMemo we can create a stable reference and also keep our data up to date using the dependency array to account for changes to the deferred promises. (This will come in handy when HMR and HDR drop in Remix.)

import { defer } from "@remix-run/node"
import { useLoaderData, Await } from "@remix-run/react"
import { Suspense } from "react"

export function loader() {
  return defer({
    deferredPromise1: fetch1(),
    deferredPromise2: fetch2(),
  });
}

function SuspendedComponent() {
  const { deferredPromise1, deferredPromise2 } = useLoaderData()
  const promises = useMemo(
    () => Promise.all([deferredPromise1, deferredPromise2]),
    [deferredPromise1, deferredPromise2]
  )

  return (
    }>
      }>
        {([promise1, promise2]) => }
      
    
  )
}

Problem solved! No more flickering. React now has a fixed reference to the promise we are creating to resolve our multiple deferred pieces.

Shout out to Javier Villanueva for working through this one with me! Hope it saved you a bunch of googling.

Caveat To useMemo

In React useMemo is not a 100% guarantee that if you pass in the same dependency that it will always pull from the cache without re-trying to evaluate the passed in computation. React reserves the right to throw the cache away whenever they feel like it will improve rendering performance. In most cases this is not a concern and will improve our performance. Unfortunately, in this case if they do that means we have to worry about the flickering problem returning if they decided to throw away the cache. That being said...this is a very small chance of that happening and originally I had explored a solution that used useRef, but I think I would rather have a small chance of flicker if React decides the cache needs to go vs ignoring any potential updates to the promises being deferred.

Another great solution

A third option to solve this problem if it is mission critical to remove any possibility of React throwing the cache away is to build your own useMemo -esque solution that allows you to control when cache is tossed. In fact someone has already written this utility. Tom Sherman has a package (suspend-concurrently ) that gives you some utilities to handle the caveat above if you are interested in using it.

I hope you enjoyed

There is a lot more coming...

If you want to get updates when I publish new guides, demos, and more just put your email in below.