The Journey of #100DaysOfCode (@sourabhbagrecha)

#Day100 of #100daysofcode

Finally!
Today I learned about React’s Suspense API and its different use cases. Basically, Suspense lets the component wait for something before rendering.
Let’s take a look at few use cases for Suspense

1. Suspense for data fetching

Since suspense lets component wait before rendering, it can be easily utilized for data fetching like this:

let pokemon
let pokemonError
// The fetchPokemon is simply calling an HTTP REST endpoint to get the details of a Pokemon
let pokemonPromise = fetchPokemon('pikachu').then(
  p => (pokemon = p),
  e => (pokemonError = e),
)

// PokemonInfo is a React Component that renders the UI to display the information for the specified Pokemon
function PokemonInfo() {
  if (pokemonError) {
    throw pokemonError
  }
  if (!pokemon) {
    throw pokemonPromise
  }
  return (
    <div>
      <div className="pokemon-info__img-wrapper">
        <img src={pokemon.image} alt={pokemon.name} />
      </div>
      <PokemonDataView pokemon={pokemon} />
    </div>
  )
}

function App() {
  return (
    <PokemonErrorBoundary>
       <React.Suspense fallback={<div>Loading Pokemon...</div>}>
          <PokemonInfo />
       </React.Suspense>
    </PokemonErrorBoundary>
  )
}

2. Suspense for Render as you fetch

One of the biggest overheads of creating a React App is to manage the state efficiently and appropriately while keeping it readable. There’s a lot more time involved (first load the code, then parse the code, then run the code, then render the component, and finally make the request).

By utilizing the Suspense API we can simply suspend the component until a promise(HTTP call) is resolved/rejected and display the content once done and we don’t have to worry about any state updates.

function PokemonInfo({pokemonResource}) {
  const pokemon = pokemonResource.read()
  return (
    <div>
      <div className="pokemon-info__img-wrapper">
        <img src={pokemon.image} alt={pokemon.name} />
      </div>
      <PokemonDataView pokemon={pokemon} />
    </div>
  )
}

function App() {
  const [pokemonName, setPokemonName] = React.useState('')
  const [pokemonResource, setPokemonResource] = React.useState(null)

  React.useEffect(() => {
    if (!pokemonName) {
      setPokemonResource(null)
      return
    }
    // Here we are simply setting the promise(returned by the createResource function) to the pokemonResource state
    setPokemonResource(createResource(fetchPokemon(pokemonName)))
  }, [pokemonName])

  function handleSubmit(newPokemonName) {
    setPokemonName(newPokemonName)
  }

  function handleReset() {
    setPokemonName('')
  }

  return (
    <div className="pokemon-info-app">
      <PokemonForm pokemonName={pokemonName} onSubmit={handleSubmit} />
      <hr />
      <React.Suspense fallback={<PokemonInfoFallback name={pokemonName} />}>
        <div className="pokemon-info">
          {pokemonResource ? (
            <PokemonErrorBoundary
              onReset={handleReset}
              resetKeys={[pokemonName]}
            >
              {/* Below we are utilizing the pokemonResource even if the promise isn't resolved yet,*/}
              {/* because anyways our <Suspense> boundary will detect this promise and it will */}
              {/* show a fallback component until and unless the promise gets resolved or rejected. */}
              <PokemonInfo pokemonResource={pokemonResource} />
            </PokemonErrorBoundary>
          ) : (
            'Submit a pokemon'
          )}
        </div>
      </React.Suspense>
    </div>
  )
}

export default App

3. Significantly improving the UX using useTransition hook along with Suspense

It’s fascinating to see how the Suspense API makes our components declarative with the least amount of effort. But that’s great for Developer Experience, what about User Experience?

The useTransition hook allows us to prevent abrupt UI changes that may cause bad UX. So, in the previous case, whenever the component has to wait for an async operation to complete we show the user a loading screen immediately.
But useTransition hook gives us the ability to pause abrupt changes for a while before showing them on the loading-screen. It helps us in smoothening the transition phase between 2 states.

// In the config, we will specify the timeoutMs for which we don't want the fallback component
// to be visible and do something else while it is pending.
const SUSPENSE_CONFIG = {timeoutMs: 4000}

function App() {
  const [pokemonName, setPokemonName] = React.useState('')
  const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)
  const [pokemonResource, setPokemonResource] = React.useState(null)

  React.useEffect(() => {
    ...
    // Whenever there's a change in the pokemonName, we want to fetch the details of the 
    // pokemon, so we will avoid this abrupt state change to a promise and we will keep the state 
    // as before unless this API responds or the timeout ends.
    startTransition(() => {
      setPokemonResource(createResource(fetchPokemon(pokemonName)))
    })
  }, [pokemonName, startTransition])

  return (
    ...
      {/* We will make the current pokemon a little blur while the state transition is taking place */}
      {/* using the isPending flag provided by the hook. */}
      <div style={{opacity: isPending ? 0.6 : 1}} className="pokemon-info">
        {pokemonResource ? (
          <PokemonErrorBoundary
            onReset={handleReset}
            resetKeys={[pokemonResource]}
          >
            <React.Suspense
              fallback={<PokemonInfoFallback name={pokemonName} />}
            >
              <PokemonInfo pokemonResource={pokemonResource} />
            </React.Suspense>
          </PokemonErrorBoundary>
        ) : (
          'Submit a pokemon'
        )}
      </div>
    ...
  )
}

4. Cache resources

In order to optimize our app by avoiding any unnecessary/redundant API calls, we can implement caching, and together with Suspense it can provide a delightful experience to our users.
Everything remains the same as before, it’s just that we will keep a local cache object in memory to store results of recently made API calls.

const pokemonResourceCache = {}

function getPokemonResource(name) {
  const lowerName = name.toLowerCase()
  let resource = pokemonResourceCache[lowerName]
  if (!resource) {
    resource = createPokemonResource(lowerName)
    pokemonResourceCache[lowerName] = resource
  }
  return resource
}

function App() {
  ...
  const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)

  React.useEffect(() => {
    startTransition(() => {
      setPokemonResource(getPokemonResource(pokemonName))
    })
  }, [pokemonName, startTransition])
  ...
}

5. Suspense Images

Sometimes, in order to provide a consistent experience across our app, we may want to load the images together with the content. It feels weird when our app has loaded all the associated information about the content and the image is still loading.
To overcome this, we can suspend that part of the app where we want to re-render a component and we will wait for it to load in the memory, and the next time this image gets requested it will be served from the cache in the browser. But for the first time, we will have to handle the image load process.


function preloadImage(src) {
  return new Promise(resolve => {
    const img = document.createElement('img')
    img.src = src
    img.onload = () => resolve(src)
  })
}

const imgSrcResourceCache = {}

function Img({src, alt, ...props}) {
  let imgSrcResource = imgSrcResourceCache[src]
  if (!imgSrcResource) {
    imgSrcResource = createResource(preloadImage(src))
    imgSrcResourceCache[src] = imgSrcResource
  }
  return <img src={imgSrcResource.read()} alt={alt} {...props} />
}

function PokemonInfo({pokemonResource}) {
  const pokemon = pokemonResource.read()
  return (
    <div>
      <div className="pokemon-info__img-wrapper">
        <Img src={pokemon.image} alt={pokemon.name} />
      </div>
      <PokemonDataView pokemon={pokemon} />
    </div>
  )
}

...
  <React.Suspense
       fallback={<PokemonInfoFallback name={pokemonName} />}
   >
        <PokemonInfo pokemonResource={pokemonResource} />
    </React.Suspense>
...

6. Suspense custom hook

How cool would it be, if we could just wrap all of the Suspense logic into its own custom hook? The idea is to basically remove the boilerplate Suspense stuff and keep our components clean.

// Custom Suspense hook
function usePokemonResource(pokemonName) {
  const [pokemonResource, setPokemonResource] = React.useState(null)
  const [startTransition, isPending] = React.useTransition(SUSPENSE_CONFIG)

  React.useEffect(() => {
    if (!pokemonName) {
      setPokemonResource(null)
      return
    }
    startTransition(() => {
      setPokemonResource(getPokemonResource(pokemonName))
    })
  }, [pokemonName, startTransition])

  return [pokemonResource, isPending]
}

// Here's how we can consume this hook in our components
function App() {
  ...
  const [pokemonResource, isPending] = usePokemonResource(pokemonName)
  ...
}

7. Suspense List for a predictable loading experience

As our app grows in size, we might want to implement Suspense across different components in our app to make sure that a single component doesn’t slow down our complete app or even worse just crashes your app.
But how can we orchestrate their execution that’s predictable.
SuspenseList to the rescue.
The SuspenseList component has the following props:

  • revealOrder : the order in which the suspending components are to render
    • can have one of the following values: {undefined}, "forwards", "backwards", "together"
  • tail : determines how to show the fallbacks for the suspending components
    • can have one of the following values: {undefined}, "collapsed", "hidden"
  • children : other react elements which render

Let’s take a look at an example SuspenseList:

<PokemonErrorBoundary
onReset={handleReset}
resetKeys={[pokemonResource]}
>
<React.SuspenseList revealOrder="forwards" tail="collapsed">
  <React.Suspense fallback={fallback}>
    <NavBar pokemonResource={pokemonResource} />
  </React.Suspense>
  <div className={cn.mainContentArea}>
    <React.SuspenseList revealOrder="forwards">
      <React.Suspense fallback={fallback}>
        <LeftNav />
      </React.Suspense>
      <React.SuspenseList revealOrder="together">
        <React.Suspense fallback={fallback}>
          <MainContent pokemonResource={pokemonResource} />
        </React.Suspense>
        <React.Suspense fallback={fallback}>
          <RightNav pokemonResource={pokemonResource} />
        </React.Suspense>
      </React.SuspenseList>
    </React.SuspenseList>
  </div>
</React.SuspenseList>
</PokemonErrorBoundary>

And that’s a wrap!

5 Likes