Taming Asynchronous UIs with React “Suspense”
In a simpler time, when a website was requested, the server fetched some data and rendered the entire HTML document. If it took a long time to fetch the data, the visitor simply had to wait before seeing anything. As AJAX became more easily accessible with the likes of Prototype and jQuery, fetching data could be deferred until after an initial document was loaded into the browser. Parts of the UI could be delivered very quickly, then the remainder could be loaded in as it became available. With the advent of modern JavaScript frameworks, this pattern is now second nature.
The Problem
While it’s great that a fast initial page load is now common practice, unfortunately, so is an overabundance of loading indicators. On fast connections, those loading indicators can appear and disappear so quickly that they create a flickering effect in our UI. This isn’t what designers typically have in mind when crafting a user experience, and fine-tuning this behavior can be very complicated. Fortunately, the React team has recognized this problem and is introducing a new API to help us tackle that complexity.
Suspense: Deferred Rendering
This new API, called Suspense, aims to simplify how we build asynchronous UIs. It gives us the ability to defer the rendering of a component until its data has finished loading, as well as control the behavior of our loading indicators. Before we get started, keep in mind that while Suspense has been merged into master as of May, it is still under active development and will likely change before its official release in React 17. In other words, don’t use this in production.
Using Suspense
First, import the createCache
and createResource
functions from the simple-cache-provider
package.
import { createCache, createResource } from ‘simple-cache-provider’;
To create a cache, simply call the createCache
function:
let cache = createCache();
Next, define a resource using createResource
, which accepts a function that returns a promise:
let PostResource = createResource((id) => {
return fetch(`http://example.com/posts/${id}`).then(response => {
return response.json();
});
});
Now that we have our cache and resource, we can create a component that utilizes Suspense:
let Post = ({ postId }) => {
let post = PostResource.read(cache, postId); return <h1>{post.title}</h1>;
}
And use it like so:
<React.Placeholder
delayMs={50}
fallback={<div class="spinner"></div>}
>
<Post postId={1} />
</React.Placeholder>
While the API is fairly simple, there’s a bit to digest here, so let’s break it down. When we render our Post
component, PostResource.read
is called and our cache
checks to see if it has data for the provided postId
. The first time it checks, however, the cache will be empty, so it throws our promise. React then suspends rendering until that promise is resolved. It seems a little weird, but this is done by utilizing a concept called error boundaries, which was first introduced in React 16.
Placeholders Made Easy
You’ll notice we also wrapped our Post
component with a React.Placeholder
component:
<React.Placeholder
delayMs={50}
fallback={<div class="spinner"></div>}
>
<Post postId={1} />
</React.Placeholder>
This placeholder component is what ties this all together and makes Suspense so awesome. While React is waiting for our data to be fetched, it renders the fallback
specified in our placeholder, which is typically some sort of loading indicator. After the fetch has been completed, it removes the fallback
and resumes rendering our Post
component. We can even put multiple Suspense-enabled components inside a single placeholder, effectively grouping multiple data fetches in a single loading indicator.
Setting a Threshold
Last, but not least, you may have noticed the delayMs
prop on our placeholder. This specifies the threshold at which our fallback
is displayed. In this example, the placeholder will wait 50ms before displaying a loading indicator. This is important because it allows us to forgo that brief, but unnecessary, flicker of our loading indicator when our fetch is effectively instantaneous, but still show one if it takes longer.
Final Thoughts
While Suspense isn’t quite ready for production, it’s certainly worth getting excited about. It gives us a simple, yet powerful, API for creating asynchronous components that scale to our users’ network and computer speeds, allowing us to provide the optimal user experience with minimal code.
To try it out yourself, check out Jared Palmer’s react-suspense-starter project on Github.