React Suspense with Chantastic
Some awesome things I learned at a remote workshop with Michael Chan
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;
- What is React Suspense and why it matters
- React Suspense ā the meat of the workshop
- Designing flexible components
- 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 š.
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>
}
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
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.
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>
)}
/>
)
}
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: