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.
- React Lazy: source code
- React Relay : source code
- React Cache (unstable): source code
- React Fetch (unstable): source code
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 } }, }}
// useSuspendPromise(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.