Building Great User Experience with React Suspense
Jake Lumetta
Posted on February 6, 2020
Since its launch, ReactJS is known for a great developer experience when working on large scale frontend apps. While there has been a lot of focus on producing error-free and scaleable frontends, there has been less emphasis on user experience in large scale applications.
However, with the release of Suspense, the React core team is more focused on developing the best user experience and doing so requires rethinking how we approach loading code and data for our apps.
What is Suspense
In React 16.6, Suspense was initially introduced with React.lazy() API allowing developers to do components based code splitting, which was done via libraries like react-loadable. Here is how it looks like:
const BlogContent = React.lazy(() => import('./BlogContent));
<Suspense fallback={<Loader />}>
<BlogContent />
</Suspense>
</ErrorBoundary>
In this example, a BlogContent component’s code will be fetched when the BlogContent is about to be rendered. The suspense boundary around it waits for the code to load. While the component’s chunk of code is downloaded, the fallback
component is rendered. This is Suspense in its simplest form. Moving forward, Suspense is much more than just wrapping the component for code splitting.
In a broader perspective, Suspense provides developers with declarative API to handle UI while code or data is being loaded. It doesn’t say how the data is fetched, but it lets you closely control the visual loading sequence of your app. Here some might feel like it is some sort of data fetching solution provided by React but that's not what Suspense is. Rather, Suspense allows developers to display the loading states in your UI but doesn’t tie the network logic to React components.
What is Suspense & Rendering
In the React community, multiple approaches have evolved to handle data fetching and rendering:
Fetch on Render
Fetch then Render
Render as you Fetch ( A new approach from Suspense)
Fetch on Render
This is the most common technique amongst React developers today. In this approach, we mount our component and then start fetching data from our server. While data is being fetched the component shows the loading state and as soon as the data is available, component re-renders with real content. One of the major pitfalls in this technique is the waterfall, becoming unavoidable at times when the application grows into a beast.
Fetch then Render
To avoid the waterfall, we tend to centralize data fetching and render once all the data is available. Though this avoids the waterfall, the user has to wait until all of that data is available. This wait might be longer than the one in the previous approach as we get more data in parallel.
Render-as-You-Fetch - Suspense!
Suspense empowers developers to start fetching and rendering simultaneously by taking away all the heavy lifting of managing intermediate states. With earlier approaches, we had to wait for fetching to finish before we start rendering. Here rendering begins as soon as we start fetching our code.
One of the key features in Suspense for data fetching is it treats code as data. With the usage of React.lazy API, it is easier to fetch our code along with our data and rendering fallback during transitional states. It eliminates the if (...) “is loading” checks from our components. This doesn’t only remove boilerplate code, but it also simplifies making quick design changes.
Suspense for Data Fetching
An important and basic tool in Suspense is data fetching coupled with fetch promise wrapping for communication status.
Before we dive in code, we need to understand a little about how Suspense looks at its dear children. As we wrap our fetch promise around, the function should return its state of pending, rejected and resolved. Here is how these states should be returned from the wrapper function:
Pending State by throwing a promise
Rejected State by throwing a JS Error
Resolved state by returning the result.
Here is our Suspense based fetching component:
function suspenseFetch(promise) {
let status = "loading";
let output;
const fetcher = promise.then(
response => {
status = "success";
output = response;
},
error => {
status = "error";
output = error;
}
);
return {
load() {
if (status === "loading") throw promise;
if (status === "error") throw output;
if (status === "success") return output;
}
};
let blog = suspenseFetch(BLOG_URL).then(res =>
res.json()
);
const MarkdownRenderer = React.lazy(‘./MarkdownRenderer’);
export default function BlogContent() {
const content = blog.load().content;
return (
<Suspense fallback={<Loader />}>
<MarkdownRenderer content={content} />
</Suspense>
);
}
As seen, our suspenseFetch
function returns a read()method that returns our data if its available or else it will throw a Promise or Error method to mention loading and error states respectively. Both data fetching and code fetching wrapped in Suspense allowing us to download both in parallel and render when both data and code is available.
Suspense As Boundaries
Versions before 16.6 React introduced a great way to handle error in components. componentDidCatch
& getDerivedStateFromError
let developers avoid apps to crash because of an error in any single component. Functional components didn’t have any such API yet. Now, Suspense can be of great help to enable error boundaries around components. Here is a small example:
An error boundary component to use across our app:
class ErrorBoundary extends React.Component {
<Suspense fallback={<Loader />}>
<BlogContent />
</Suspense>
</ErrorBoundary>
This component can now wrap any component in our app and to manage error boundaries. Example:
<ErrorBoundary fallback={<h2>Could not fetch posts.</h2>}>
locale="en"
textComponent={React.Fragment}
messages={translationsFromLocale}
>
<App />
</IntlProvider>,
document.getElementById("app")
);
This boundary will now catch both rendering errors and Suspense data fetching errors. As Suspense will get reject status from an underlying promise, the error boundary above will catch it to display a declared fallback UI.
Is it Production Ready?
Though Facebook.com’s rewrite is completely based on Suspense and Relay as said by the React team, it is an experimental feature and APIs might change drastically before it gets stable. Nevertheless, it is something that developers who like to move fast and break must try. Suspense and the Concurrent mode are surely turning around the frontend world to a large degree. This shift will bring in a lot of new ideas that will make it easier for a frontend developer to cope up with the demands of great experiences.
What lies Ahead
Suspense is still very new to the frontend world and best practices are still in process of development. As we move forward and the community adopts Suspense more, many patterns will evolve to enable great user experiences.
Posted on February 6, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.