Fetching data, showing loading states and rendering responses are traditionally all responsibilities of the React developer. Announced at JSConf Iceland, the new “Suspense” feature moves many of these responsibilities into React itself.
My first reaction to this announcement was one of irrational fear-of-change.
“These are things a developer can already do, why does React need to provide them?”
Emotional knee-jerks aside, my curiosity remained. Needing to know how Suspense works, I decided to see how closely I could recreate it in React 16.2.
Oh the suspense
First, a refresher on how suspense is used.
CharacterList component renders a list of Star Wars characters. To get this data, it calls
read on a “fetcher” which is created by one of the proposed React features
read is called and the data has not been resolved, React will suspend that render until the data becomes available. This behaviour is invisible to the developer. From their perspective, they happily call
read and the data is there. On the library’s side of the fence, this is much harder to take care of.
We know that
read must return a value. We also know when we don’t have a value, the fetcher should tell React to suspend rendering. Working out how to communicate “suspend rendering please” from the context of this function was the biggest head scratcher for me.
The answer is to throw a Promise.
When a fetcher is created we setup a cache that maps arguments passed to
read to the resolved values. When
read is called the fetcher checks whether there is a value for the argument. If there is, it returns the value immediately. If there isn’t, it throws a Promise that executes the fetch and adds the value to the cache when it resolves.
As bizarre as this may look, throwing a Promise is the crucial link between the fetcher and the React world.
If we make sure our fetcher throws the Promise within a React error boundary, we can catch that Promise and now be in the context of a React component. The
Loading component shown in the JSConf demo is probably the simplest example of this.
The implementation of
Loading can be cobbled together like:
componentDidCatch is called,
Loading receives the Promise thrown by the fetcher. It uses a render prop to communicate to it’s children whether a fetch is in progress or not. We are now able to change render behaviour based on the status of a fetcher. 🙌
deferSetState function was demoed as a way for components to indicate that the next render might result in a fetcher executing. To create a use-case for this, let’s expand
CharacterList to have pagination. When the user clicks on the previous or next buttons, nothing on the UI should change until the new characters are fetched and then rendered.
I was not able to replicate the same syntax as shown in the demo so I created a
DeferredState component that provides the
deferSetState function instead.
The responsibility of
DeferredState is to manage any state changes that could result in a fetcher throwing a Promise. It does this by providing the
deferSetState function and “owning” the state that is passed to fetchers.
deferSetState is called,
- Know the current state of
- Know the next state of
- Try to render
childrenwith the next state. This may throw a Promise.
- Catches the Promise and renders
childrenback in current state.
- When the Promise resolves, next state becomes current state and
The implementation looks like:
The key point is that
DeferredState can recover from a fetcher throwing a Promise by rendering a state it knows will succeed. An interesting result of the children-as-function approach, is that we can use a try-catch statement rather than error boundaries.
With all this in place, we have nailed our use-case. When the user navigates between pages, rendering the character list is “suspended” while the next page is fetched. 🎉
Why does React need to provide Suspense?
What I have shared is a fragile attempt at re-creating Suspense based on my understanding. That understanding was informed by watching one video and reading a bunch of tweets. I am happy with what I have done — I found it interesting and hopefully you have too.
Now to the question of whether there are any technical reasons why Suspense should be shipped in React. I can’t give an informed answer to this. I don’t know enough about how this feature works in reality. Maybe there are no technical reasons. Whether this is the case is not important.
What is important, is the React team being engaged in the community, recognising best practices and making these practices easier for everyone to follow. Suspense is just the latest example of this.
I can’t wait to get my hands on the real thing.
Update: why React must provide Suspense
As it turns out, there are technical reasons for React to provide Suspense. Let’s update the
In the above example we have added a
CharacterListWithCounter component. The call to
read now happens in the render method of
CharacterListWithCounter. In the original example,
read was called from the render method of
read to a child component means the Promise thrown by the fetcher is not caught in the try/catch of
DeferredState. Instead, it is caught in the error boundary that has been added to
This is cool - we can now
read from a fetcher anywhere in the component tree below
DeferredState. Except that adding this feature exposes a fundamental problem of implementing Suspense outside of React. 😅
Taking another look at the example,
Counter is a simple component that shows a button and the number of times it has been clicked. It keeps track of this number in it’s own state. Every time we navigate to a new page of Star Wars characters, we see that this counter is reset. 🤔
componentDidCatch is called, React will have unmounted the component tree underneath the error boundary. Unmounting a component will blow away all of it’s state. This is the reason why
Counter is being reset each time we navigate between pages.
A possible work around would be to lift the state of all components below
DeferredState. That way, the component tree could be mounted with the same state it had when it was unmounted. This solution is painful at best, and impossible if you are rendering a 3rd party component with state.
From inside React, the internal state of each component is known. This makes recovering from a fetcher exception easier. It can use the internal component states to remount the component tree exactly how it looked before the exception. Alternatively, if React knows that the exception came from a fetcher, it might be able to avoid unmounting the component tree altogether.
Special thanks to Dan Abramov from the React team for pointing this reason out to me. With this update, I feel that I have satisfied my curiosity on the subject.
Thanks for reading!
More questions? Reach out to me on twitter @pete_gleeson