Understanding React “Suspense”
At the stage of last JSConf.is conference, Dan Abramov unveiled a new feature / API for React called React Suspense. In case you missed it, you can re-watch the talk on YouTube:
In short: the new feature allows you to defer rendering part of your application tree until some condition is met (for example data from an endpoint or a resource is loaded).
Why do we need this?
Delaying displaying content is possible already, all one has to do is check if the data is loaded, and if not, show a spinner — if it’s as simple as that why do we need this?
In reality things are a bit more complicated than that:
- with this approach we need to either implement the loading screen in the component which requests / consumes the data, or expose its API to parent
- things get more complicated the more content we need to load (what if you wanted to load a movie details page only when the reviews and all images are also already loaded?)
- once we start making use of AsyncMode things will get even more complex — parts of your application might have new data, parts of it old, which would lead to “tearing” or “flickering” of content
In order to fix those, and probably more issues which will become more apparent once AsyncMode becomes mainstream React team proposed the new API.
Another big issue that this solution aims to address is the user experience difference between users with fast device/network and slow ones. If we do go with the spinner that was our initial idea, we might end up showing it for a fraction of a second for users that have fast connection, in which case not showing it at all would be better UX as it eliminates screen flickering.
Please note that the current API is not final and will more than likely change before public release.
Let’s step through the code
Most of the code should be easy to follow (remember to also check the comments :)) but there are some things created just for Dan’s demo or introduced in the alpha of React.
Resource fetcher / cache provider / “abstractor of the API”:
The two functions you see here are provided by simple-cache-provider package created by Andrew. Here’s what exactly it’s doing:
- create a shared cache object
- create a “resource fetcher” (createResource) which you call with a set of parameters
- return a promise that will resolve after a set time, with the input value
This is a really simple example, which in a real-world app would probably be replaced by an API call or similar mechanism. The most important part is that the
readText function which once called, will either directly return the value it resolves to (not the Promise!) or would throw a Promise.
Wait — what?
Yes, you read that right, the function either returns the value, or throws — so you can do:
The call to
readText will either resolve to the Promise value or will end up throwing the Promise.
Since this is not apparent (it’s hidden inside
simple-cache-provider), you can imagine another version of the
As we know, a React Component can only return JSX (or null) — so we need a method of communicating that we’re “not yet ready” to any parent that is prepared to handle that status. Sounds a bit familiar? If it does, that’s because React already has a mechanism that allows you to throw in a component and let its parent or grand-parent handle this — Error Boundaries. Suspense is reusing some of this API for its own purposes.
Using the component
If we were to use the component right away, React would warn us that we’re “doing it wrong”:
In order to properly render the component, we need to import a new React primitive component and create a helper for it:
We’re using the new
Timeout component which accepts two input props:
msprop, which indicates the time after which we want to see the fallback content (here passed to the
Loadercomponent as prop!)
childrenwhich should be a “function as a child” or “render prop” function; this function will be called with one parameter, which indicates if the specified time elapsed
Let’s take a dive into what’s actually going on there. When this component gets rendered, React will call the child function right away with a value of
false which will allow us to render the child component.
Once the child component renders, it will try to read the data, find out that it’s not there and throw the promise. At this point React catches the Promise, waits the correct time (provided as
ms prop) and runs the function again, this time with
true so that we can show the fallback.
Once the Promise resolves, the function is called yet another time, again with
false which end up rendering the correct component.
We could go without creating this helper and inline it all in our component, but this makes it more reusable and easier to read when actually …
Using the components
Putting the components together gets rid of the error from previous sections and allows us to have a placeholder shown while the data is being “fetched”. There’s only one issue though — if we were to simply use
setState to toggle the visibility of our component (or render it right away) the placeholder would be shown right away — not after the specified time.
Here we get to the last part of the puzzle:
ReactDOM.unstable_deferredUpdates is a callback which was introduced back in initial React Fiber release (so 16.0) which allows you to schedule an update with low priority. This is the part that actually allows the Timeout component to “wait” before re-rendering itself.
What to expect?
A week before Dan’s talk, Andrew Tweeted the following:
Was he right? I think it was. This addition to React tool-set helps us solve a lot of problems we are facing today such as:
- data fetching, without locking the UI and at the same time allowing users to change their decision while data is being fetched (which could result in race conditions)
- providing good experience to users with low-end, high-latency devices
- creating a common abstraction over “loading” state
Once the API gets finalized, we will probably see a lot of libraries pioneering the solution and will have to adjust our best practices (suddenly calling AJAX requests inside render will become OK) but I believe this will allow us all to create even better applications and experience for users!