React Suspense with Chantastic

Some awesome things I learned at a remote workshop with Michael Chan

Taran "tearing it up" Bains • October 16, 2019

Hey y’all (most likely just me) but here’s a high level overview of the things I learned from the remote egghead worskhop I attended today.

Here's a link to the repo

TLDR;

  1. What is React Suspense and why it matters
  2. React Suspense — the meat of the workshop
  3. Designing flexible components
  4. Nuggets picked up from the workshop

What Is React Suspense and Why It Matters

Prior to the advent of Webpack and other bundlers, code-splitting — the ability to “split” our JavaScript bundles into mutliple chunks — was something that was not possible. However, thanks to the aforementioned tools, code-splitting is a commonly used practice that helps to ensure that the JavaScript bundles that our users receive are as small as possible; this equates to better performance! We all know that the fastest code is no code… so less code means faster code 😉

For example if we have a Single Page Application (SPA), prior to React Suspense, a common practice was to use a package like React Loadable to handle splitting the various routes for said application*. It would be wasteful to load all the code for every route at initial load time/runtime because there could be several routes that a specific user never ends up actually visiting — what a waste! 😱.

There is one thing that Suspense requires to work though: a cache! Currently, there is no stable caching pacakge for React, but don’t you worry, the React team is working diligently on a package that will plug in and work with Suspense when it lands. Why do we need react cache? Simply put, Suspense needs to know if a promise has resolved so under the hood react cache will throw a thenable(promise) and until the thenable has resolved, Suspense will show sort of fallback.

Chances are if you’ve ever written a component that required data from some external resource, you’ve made an API request for that data and you’ve rendered your component with the data once it was fetched. However, once that component had unmounted and been destroyed, the data that was tied to that component went with it! If that component then would need to be mounted again, you’d have to potentially make the same API request for the same data and while browsers have implemented caches for network requests, that doesn’t really help with our components rendering! But with react cache, if the said data had been requested earlier the data will be available in the in memory cache and there will be no need to fetch the data from the network request. 👍

Once Suspsene and react-cache have stabilized, we will no longer need to worry about the logic regarding async resource loading!

*This is assuming that the SPA in question has multiple routes that users can reach.

React Suspense — The Meat of the Workshop

I’m just going to show the code and explain it below😁.

pokemon.js
import React from "react"
import { unstable_createResource as createResource } from "react-cache"

let PokemonResource = createResource(() =>
  fetch("https://pokeapi.co/api/v2/pokemon/x").then(res => res.json())
)

export default function Pokemon() {
  return <div>{PokemonResource.read().name}</div>
}

index.js
import React from "react"
import ErrorBoundary from "./error-boundary"
const Pokemon = React.lazy(() => import("./pokemon"))

export default function() {
  return (
    <React.Fragment>
      <ErrorBoundary fallback={<h1>...couldn't catch 'em all</h1>}>
        <React.Suspense fallback="Locating pokemon...">
          <Pokemon />
        </React.Suspense>
      </ErrorBoundary>
    </React.Fragment>
  )
}

Pokemon.Js

In this file we are creating and exporting a very simple component Pokemon. This component is meant to display some data in a div — not very inspired but that’s not the point of this blog post and it wasn’t the point of the workshop either.

On line 4 we’re using the createResource function that we imported from react-cache and providing it with a function as an argument. The caveat with the function argument is that it needs to return a thenable; luckily, the modern fetch implementation that ships with browsers or packages such as axios provide us with thenable implementations.

Remember what I said above, we need to provide Suspense with a component that will throw a thenable! Thanks to our createResource call, if we attempt to read from the resource with read prior to the promise being fulfilled, createResource will throw the promise and Suspsene will catch it and do what it will… in this case, show some sort of fallback component or text.

Index.Js

In this file, we see Suspense shine. There’s two components that we need to be paying attention to here: ErrorBoundary and React.Suspense.

Errorboundary

We’ll start our discussion here as this component is higher up in the tree. Remember how I mentioned above that Suspense expects the component it lazily loads to throw a thenable? Well ErrorBoundary is a component that when an error is thrown, it catches and responds to it. The reason why this component wraps our Suspense component is if for whatever reason our lazily loaded component(s) that Suspense wraps throws an error rather than returning a value or throwing a promise, we need a way for our UI to react to this. ErrorBoundary is the component that allows us to react!

Please not that ErrorBoundary cannot be a functional component — React requires it to currently be implemented via a class

ErrorBoundary.js
import React from "react"

export default class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }
  }

  componentDidCatch(error, errorInfo) {
    this.setState({
      hasError: true,
    })
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback
    }

    return this.props.children
  }
}

ErrorBoundary.defaultProps = {
  fallback: <h1>Something went wrong.</h1>,
}

Designing Flexible Components

A big piece of knowledge/wisdom I was able to pick up from this workshop regards components in the post react age.

Each component should have one specific intent

What this means to me is that a button component’s intent is to fire off some click event once clicked; we leave it up to the user of the component to decide what this click event will be. If we abstract this idea out even further, if we have a component that is responsible for rendering a list of data, we should allow the user of this component to dictate what the rendering function will look like for a data item.

index.js
import React from "react"
import { PokemonList } from "./PokemonList.js"

function App(props) {
  return (
    <PokemonList
      as="ul"
      renderItem={pokemon => (        <li key={pokemon.name}>
          <button onClick={() => alert(`You selected ${pokemon.name}`)}>
            {pokemon.name}
          </button>
        </li>
      )}
    />
  )
}
PokemonList.js
import React from "react"
import { unstable_createResource as createResource } from "react-cache"

let PokemonCollection = createResource(() =>
  fetch("https://pokeapi.co/api/v2/pokemon").then(res => res.json())
)

export function PokemonList({
  as: As = React.Fragment,
  renderItem = pokemon => <div key={pokemon.name}>{pokemon.name}</div>,
  ...restProps
}) {
  return (    <As {...restProps}>{PokemonCollection.read().results.map(renderItem)}</As>
  )
}

Notice how on line 8 of index.js we have provided a renderItem property to the PokemonList component. This type of inversion of control is called, at least to the best of my recollection, dependency injection. Michael’s feelings are that any components that are iterable or iterate over some data should provide a capability for its users to override the default rendering behavior for each item. That’s what the highlighted line in PokemonList.js is doing! As shown on line 12, the renderItem attribute on props, which defaults to a function, allows a user to define their own map function.

Another nice thing that PokemonList allows its users to do is decide what the base container component should be; that’s what the as property is! Similar to what is done in styled components, we can say that a component should render itself “as” another valid JSX component.

This kind of thinking when designing components will help the developer who has to create resources (functions and components in React which are actually just functions) that start general but allow for specificity.

Little Nuggets from the Workshop

At the end of the workshop, Chantastic invited us all to ask some questions and like most question and answer segements, this one was ripe with golden nuggets. The following are 4 libraries that Micahel Chan (Chantastic) recommends to teams he works with:

  1. ReachUI
  2. React Router
  3. Immer
  4. react-beautiful-dnd