Loading Fast and Slow: async React Server Components and Suspense
React Server Components promise a lightning-fast web. And they are, so long as you use them properly.
React Server Components are a game-changer when it comes to building large web applications without sending megabytes of JavaScript to the client. They allow you to render components on the server and stream them to the client, which can significantly improve the performance of your application.
However, React Server Components can throw errors, just like regular React components. In this article, we'll explore how to handle and recover from errors in React Server Components.
In React, you can use error boundaries to catch errors that occur during rendering, in lifecycle methods, or in constructors of the whole tree below them. An error boundary is a React component that catches JavaScript errors anywhere in its child component tree and logs those errors, displaying a fallback UI instead of crashing the entire application.
To create an error boundary in React, you need to define a component that implements the componentDidCatch
lifecycle method. This method is called whenever an error occurs in the component tree below the error boundary.
Here's an example of an error boundary component:
Alternatively, you can use the ErrorBoundary
component from the react-error-boundary library, which provides a slightly more robust implementation of error boundaries, including support for error recovery and retrying rendering. Here's how we might use that on an RSC-rendered page:
When we render this page, we'll end up seeing something like this:
This is useful as it allows the rest of our page to render and be usable, even if a component or two throw errors. It allows us to inform the user that something went wrong, at which point they're likely to want to hit the refresh button. But there's a better way...
Wouldn't it be cool if we could allow the user to retry rendering the component that errored out? With React Server Components, we can! Kind of.
Our ideal solution here would be to allow the rest of the page to render and be interactive, while the errored component is replaced with a button that allows the user to retry rendering it. If we're going to show the user an error, it's best not to take down the whole page with it, and to give them an easy way to recover from it.
Let's see how we might implement this:
Ok so we have have a page that contains a bunch of content, plus a component that has a 50% chance of throwing an error. If it does, we'll show a Reset button that the user can click to retry rendering the component.
We used the ErrorBoundary
component from react-error-boundary to catch the error and display the ErrorFallback
component when an error occurs. The ErrorFallback
component contains a button that allows the user to retry rendering the component. Here's what the ErrorFallback
component looks like:
A few things to note here:
ErrorFallback
component is a client component (so is its parent - the ErrorBoundary
component that we used)router.refresh()
to retry the rendering of the component that errored out. This actually re-renders the whole page, but to the user it looks like only the errored component is being re-renderedrouter.refresh()
call in the new startTransition
API because router.refresh()
is a long-running operation that does not return a Promise, so we can't await
itisResetting
state variable to allow us to show a spinner while the component is being re-renderedWhen we render this page, we'll see something like this:
That's a fully interactive iframe pointing to a live example on my RSC Examples site. You had a 50% chance of seeing an error, but you can hit the little refresh icon above the iframe if you got lucky/unlucky enough not to see an error.
Now, when you click the blue Retry
button, our retry
function within the ErrorFallback
component will be called. This will set the isResetting
state to true
, refresh the page, reset the error boundary, and then set isResetting
back to false
. This will cause the ErrorComponent
to be re-rendered, and with a bit of luck, it won't throw an error this time.
What actually happened here is that we reloaded the whole page, so it's not as surgical as it looks (the hint is in the router.refresh()
call...). However, from the user's perspective, it feels very much like just this one single component is being retried, and existing state such as form input is maintained. This is significantly better than either crashing the whole page or forcing the user to refresh the whole page.
The combination of error boundaries and retrying rendering with React Server Components allows you to build robust web applications that can recover from errors gracefully. By catching errors and displaying a fallback UI, you can prevent your application from crashing and provide a better user experience.
If you found this post insightful, you might enjoy exploring Promises across the void: Streaming data with RSC, which delves into managing asynchronous rendering in React Server Components. Additionally, Decoding React Server Component Payloads provides a detailed look at how Next.js handles server-client communication through React Server Components, enhancing your understanding of server-side rendering intricacies.
React Server Components promise a lightning-fast web. And they are, so long as you use them properly.
React Server Components let you send unresolved promises from server to client.
More than you ever need to know about how React Server Component Payloads work.
Markdown is a really nice way to write content like blog posts and other long-form content, with live components inside