5 min read

How To Take Advantage of React Suspense?

In the last few months, I've been working on and building a React Data-Fetching library.

  • Temporary name: react-retain

My To-Do list grew longer and longer as my research progressed. With implementing the features in it, I realized that it was necessary to record something about the React Suspense.


In brief, I wanna talk about how to take advantage of React Suspense, instead of how to use it.

This article assumes that you are already familiar with the following concepts:

  • React 16/17/18 concepts of main topics (like Scheduling Model/Rendering Model)
  • The pain points for UI Engineering

🙋‍♂️Hang on, What is React Suspense At First?

Suspense is not a data fetching library in React. It's a mechanism for data fetching libraries to communicate to React that the data a component is reading is not ready yet.


This is an official detailed description for library authors:

Suspense for Data Fetching (Experimental) - React

Suspense for data fetching is still developing and the behaviors could be changed.


Let me talk about more benefits of that.

Benefits:

Suspense is an improvement to the developer experience when dealing with asynchronous data fetching within React applications

Suspense also transforms the way we think about loading states, which shouldn't be coupled to the fetching component or data source (Which means it beyond API data fetching and can be applied to any async data flow)

If you have read the React Roadmap carefully, you will see that there is already an official description of this part at the end of 2018

React 16.x Roadmap - React Blog


What is Suspense Actually Doing?

React has been throwing around this idea that a component can throw a Promise.

throw is very important here, it can't return a Promise, it must throw it to indicate it's "async".

If the nearest ErrorBoundary (which Suspense is) catches an rejected (or unresolved) Promise, then it displays it's fallback.

When the Promise it caught resolves, it renders it's children

That's what Suspense does, tremendously simple when you think about what it's actually doing.


Let's look at a few examples of React official usage of Suspense

These can fetch the data source/component and suspend these.


Then let's write our implementation for a data fetching library.

function SuspendPromise(promise) {
let status = 'pending'
let result
let suspender = promise.then(
(r) => {
status = 'success'
result = r
},
(e) => {
status = 'error'
result = e
}
)
return {
read() {
if (status === 'pending') {
throw suspender
} else if (status === 'error') {
throw result
} else if (status === 'success') {
return result
}
},
}
}
// use
SuspendPromise(fetcher()).read() // proxied data maybe better

And React Suspense handles throws

Suspense: soure code

BTW, When the data has been resolved, React internally will call the callback function to notify the component that the data has been resolved and ready to refresh.

attachPingListener: facebook/react@0e100ed/packages/react-reconciler/src/ReactFiberThrow.new.js#L161-L190


And what happens in React 18?

When the user re-fetches data in a valid, stable UI, we wouldn't show a loading indicator again, It hardly a good UX, We'd prefer the old UI keep on the screen while new data fetching.


In React 18, State updates in two categories.

  • Urgent
  • Non-Urgent

Non-Urgent updates could be called Transition updates, it will update states in memory/in the background while keeping your existing UI on screen, and keeping your app responsive during large screen updates.


React provides two API (for the time being) to explicitly choose how to transition updates:

  • startTransition
  • useTransition (hook)

(In fact, the two API's do the same thing. startTransition is a common function, it could be called in most library code, and you can only use useTransition inside of a component, If you do use the hook without using the isPending flag then there's a bit of unnecessary work because React still re-render to update the isPending value. And it's just a hook, so follow the hook rules)


Suspense and startTransition/useTransition, they are independently useful features, and they are designed to work together.

If you don't use startTransition/useTransition when the page re-suspends during a refresh, React will replace the stale content with a placeholder(Suspense's fallback).

However, by wrapping the update with startTransition/useTransition, you're telling React that don't remove existing UI, just wait until the updated data has resolved and display new UI directly. (Whether you use Suspense)

In the early experimental version, useTransition takes one parameter: timeoutMs. This indicates the amount of time you’re willing to let the in-memory state change run, before it suspend.

As you probably don't want your existing UI to show indefinitely while your loading is pending.

In the latest experimental version, useTransition no longer takes parameters, the timeout behavior will be controlled by react.

And one more thing, Suspense has more features on the server in React 18.


Conclusion

In this article, we've taken a look at what exactly Suspense does.

Suspense is an interesting concept that makes errors and async handling declarative.

Suspense maybe is a stretch from Algebraic Effects


Ideally, we throw the Promise when it isn't resolved, React catches that Promise (like <Suspense> and <ErrorBoundary> did), and remembers to retry rendering the component tree after the thrown Promise resolves.

In fact, If the Promise is resolved (which means the data is ready), and nothing is thrown. <Suspense> and <ErrorBoundary> will render their children, instead of fallback.


One thing that cannot be missed in this process which closer-to-reality is that React ensures that the re-rendering is idempotent.