Skip to Main Content
TWIL
React
Featured image for TWIL blog post on React Debugging and UI enhancement with React Suspense.

Welcome to TWIL, our weekly software craft chronicle where development wisdom sprouts from hands-on encounters with code. This edition has Marisa demonstrating two React techniques. First, she guides us through Forcing Suspend State for Debugging, breaking down the utility of React DevTools to streamline browser debugging. Then we're learning how to Use React Suspense to Simplify Your Async UI, laying out how Suspense and error boundaries can enhance asynchronous user interfaces, affording a more fluid and responsive experience.

Forcing Suspend State for Debugging

You can utilize the Chrome DevTools to force your Suspended component into its suspend state to perform debugging in the browser. To do this, first navigate to the ⚛️ Components tab:

Navigate to the specific component, and select it. Once there, you can click on the timer icon to invoke the suspended state:

Now your component will be in its suspended state!

  • React
  • Tools
Marisa Gomez's profile picture
Marisa Gomez

Senior Software Engineer


Use React Suspense to Simplify Your Async-UI

React Suspense lets components “wait” for something before rendering.

For this example, ensure you have a version of React that supports concurrency - React (and React-DOM) v18 and up.

Setup Concurrent Rendering

In index.js, or where you render the root of the application, update to use the following flow:

// Old Way
const rootElement = document.getElementById('root')
ReactDOM.render(<App />, rootElement)


// New Way
const rootElement = document.getElementById('root')
const root = ReactDOM.createRoot(rootElement)
root.render(<App />)

Error Boundaries

When you utilize Suspense in your component, you should also set up an Error Boundary component. This wrapper component will catch any JavaScript error thrown from its child components, and display a fallback UI.

Note: Error Boundaries must be Class Components.

class ErrorBoundary extends React.Component {
  state = { error: null }

  static getDerivedStateFromError(error) {
    return { error }
  }

  componentDidCatch() {
    // log the error to a server
  }

  render() {
    return this.state.error ? (
      <div>
        There was an error.
        <pre style={{ whiteSpace: 'normal' }}>{this.state.error.message}</pre>
      </div>
    ) : (
      this.props.children
    )
  }
}

Available library: https://github.com/bvaughn/react-error-boundary#readme

Simple Data-Fetching Example

In this example, we are fetching data on a specific Pokemon. When the App mounts, we want to kick off the request for the Pokemon information, displaying a loading message while we wait, and catching any errors that arise.

import React, { Suspense } from 'react'

// Utils & Service
import { ErrorBoundary, PokemonDataView } from '../utils'
import fetchPokemon from '../fetch-pokemon'

// "Fetch" our pokemon
let pokemon
let pokemonError
let pokemonPromise = fetchPokemon('pikachah').then(
  // Handle success
  p => (pokemon = p),

  // Handle error
  e => (pokemonError = e),
)

const PokemonInfoCard = () => {
  // If there's a `pokemonError`, throw it
  // This error will be caught by our `ErrorBoundary`
  if (pokemonError) throw pokemonError
  
  // If there's no `pokemon`, throw the promise
  // This promise will be caught by our `Suspense` wrapper
  if (!pokemon) throw pokemonPromise

  // `pokemon` is available, render the information
  return (
    <div>
      <div className="pokemon-info__img-wrapper">
        <img src={pokemon.image} alt={pokemon.name} />
      </div>
      <PokemonDataView pokemon={pokemon} />
    </div>
  )
}

function App() {
  return (
    <div className="pokemon-info">

      {/* Wrap our implementation in `ErrorBoundary`, this will handle catching and displaying errors for us */}
      <ErrorBoundary>
      
        {/* Wrap our `PokemonInfoCard` in a `Suspense` component, this will handle displaying the loading state */}
        <Suspense fallback={<div>Loading Pokemon...</div>}>
          <PokemonInfoCard />
        </Suspense>

      </ErrorBoundary>
    </div>
  )
}

Suspense lets you specify what you want to display while the children in the tree below it are not yet ready to render. You can display a loading indicator or some placeholder UI with animations.

Writing a Generic Resource Factory

We can update our original fetch implementation and make it more reusable by turning it into a function that handles all of the cases.

const getFetchedResource = asyncFunc => {
  // Create a `status` to be monitored for returns
  let status = 'loading'

  // Kick off the specified `asyncFunc`, handling
  // successes and failures
  let result
  let promise = asyncFunc().then(
    r => {
      // Update `status` and `result`
      status = 'success'
      result = r
    },
    e => {
      // Update `status` and `result`
      status = 'error'
      result = e
    },
  )

  return {
    read() {
      // Use `status` to determine what to throw or return
      if (status === 'loading') throw promise
      if (status === 'error') throw result
      if (status === 'success') return result
    },
  }
}

We can now update the rest of our original example to utilize this generic helper.

// Wrap our `fetchPokemon` call in our helper method
const resource = getFetchedResource(() => fetchPokemon('pikachu'))

function PokemonInfo() {
  // Invoke the `read` function, triggering the API call
  const pokemon = resource.read()

  // `pokemon` is available, render
  return (
    <div>
      <div className="pokemon-info__img-wrapper">
        <img src={resource.image} alt={resource.name} />
      </div>
      <PokemonDataView pokemon={pokemon} />
    </div>
  )
}

Since our PokemonInfo component is wrapped in our Suspense and ErrorBoundary, any result thrown from within getFetchedResource will be taken care of.

Improve Suspense Loading States with useTransition

Now that we have generalized our implementation, making it more reusable, what happens when we want to get updated or new information? By default, React is optimistic when waiting for your suspending promise to resolve. This causes additional lag after the first render of your suspended component. After making a change to trigger the suspended promise, React is optimistic that the result will come within 100ms, but doesn’t take into account a longer waiting period. To help with this, you can use the useTransition hook, allowing you to handle the pending state yourself.

A couple of notes here:

  • Updates in a transition yield to more urgent updates such as clicks.
  • Updates in a transition will not show a fallback for re-suspended content (that’s why we need to handle the isPending state), allowing the user to continue interacting while rendering the update.
function App() {
  // State 
  const [pokemonName, setPokemonName] = useState(null)
  const [pokemonResource, setPokemonResource] = useState(null)
	
  // Transition
  const [isPending, startTransition] = useTransition()

  // Handle new submissions of `pokemonName`
  const handleSubmit = (newPokemonName) => {
    setPokemonName(newPokemonName)

    // When a `newPokemonName` is entered, we want to start
    // a new transition, creating a new resource to kick off
    startTransition(() => {
      setPokemonResource(getFetchedResource(newPokemonName))
    })
  }

  return (
    <div>
      <PokemonForm onSubmit={handleSubmit} />
      <hr />
      {/* Utilize `isPending` here to update styles with the loading state */}
      <div style={{opacity: isPending ? 0.6 : 1}} className="pokemon-info">
        {pokemonResource ? (
          <ErrorBoundary>
            <Suspense
              fallback={<PokemonInfoFallback name={pokemonName} />}
            >
              <PokemonInfo pokemonResource={pokemonResource} />
            </Suspense>
          </ErrorBoundary>
        ) : (
          'Submit a Pokemon'
        )}
      </div>
    </div>
  )
}

The useTransition hook is also useful when you only want to transition a certain piece of UI, leaving the remaining UI available to the user to interact with.

  • React
Marisa Gomez's profile picture
Marisa Gomez

Senior Software Engineer

Related Posts

Image from the "TWIL" blog post showing how Firebase tools integrate with JavaScript and React-Native for app development.
October 29, 2019 • Frank Valcarcel

TWIL 2019-10-25

Join us for this week’s “TWIL” where Marisa guides us on how Firebase’s Firestore, Storage, and Cloud Functions work in unison with JavaScript and React-Native to update documents in real-time applications.

IoT Security
November 3, 2016 • Nick Farrell

We Need a Standard for IoT Security

Halloween came early as an army of zombified IoT bots took down major websites from Netflix to Amazon. Real-life poltergeist, or a ghost in the machine? In case you haven’t heard, here’s a rundown of what happened.