Let’s build a React from scratch: Part 3— React Suspense and Concurrent Mode

Arindam Paul
14 min readMar 30, 2022

--

Disclaimer: The content below is just for learning and have some key insights while using framework like React. While the inspiration is React, the idea is to deliver the core concepts of what it takes to build a library like React at its most basic level.

Hello there! Welcome to the 3rd Part of this Series, which talks about Performance a bit while using libraries like React. What are the traditional bottlenecks for large applications and how React is planning on solving them by introducing React Suspense and Concurrent Mode? To be honest, I didn’t plan the timeline but it so turned out that React 18 which packs all these awesome features just got released yesterday for general usage.

So, it will be an amazing time to talk about them and have a deep understanding of how these actually work. The bottom line, it can make your app really fast and predictable even with slow computing or slow network connections.

Concurrent rendering also unveils the door for progressive server-side rendering as well. Now all of these might seem alien to you (at least, it was to me), so as always we will start from the basics and build our way up to all these by implementing them. This time, there will be a little bit of theory before hands-on coding, but the concepts will go a long way in helping us build what we are aiming for. So, let’s get started...

🦁 React Rendering Techniques 🐒

In the first section, we talked about how we take a VirtualDOM and render it out to the Browser. But sometimes(most of the time :)), you have to fetch the data from some remote API before you can use that to render it to the DOM. Now, in general, in react for these there are three primitive rendering techniques, let’s discuss them briefly here,

Approach 1: Fetch-on-Render (not using Suspense)

The first, and most traditional approach would be to manage the fetching after the initial render and when the data is ready, we populate the same using our States in React. Let’s see a quick example,

function ProfilePage() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(u => setUser(u));
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline />
</>
);
}

We call this approach “fetch-on-render” because it doesn’t start fetching until after the component has rendered on the screen. This leads to a problem known as a “waterfall” if you have dependent data to be fetched. It will unwrap and re-render one layer at a time.

Approach 2: Fetch-Then-Render (not using Suspense)

Libraries can prevent waterfalls by offering a more centralized way to do data fetching. For example, we can solve this problem by moving the information about the data for a component into a separate dedicated function call altogether. Fundamentally, we are still using the setState only to signal and trigger the rendering process. Let’s take a look at this example, from, react docs,

// Wrapping all data fetching
function fetchProfileData() {
return Promise.all([
fetchUser(),
fetchPosts()
]).then(([user, posts]) => {
return {user, posts};
})
}
// Using it in our Component
function ProfilePage() {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState(null);
useEffect(() => {
promise.then(data => {
setUser(data.user);
setPosts(data.posts);
});
}, []);
if (user === null) {
return <p>Loading profile...</p>;
}
return (
<>
<h1>{user.name}</h1>
<ProfileTimeline posts={posts} />
</>
);
}

Approach 3: Render-as-You-Fetch (using Suspense)

In the previous approach, we fetched data before we called setState . So, the fundamental sequence of operation is something like this, where render() call is always the last step.

  1. Start fetching
  2. Finish fetching
  3. Start rendering

With Suspense, we still start fetching first, but we flip the last two steps around:

  1. Start fetching
  2. Start rendering
  3. Finish fetching

With Suspense, we don’t wait for the response to come back before we start rendering. In fact, we start rendering pretty much immediately after kicking off the network request.

This is a fundamentally different mental model where you as a user don’t have to think or work on optimizing the data fetching (batching) and keep them in the state just so that you can trigger the render once fetching is complete.

Also, this opens the door for not only fetching data but fetching all types of resources like images, other pages, documents etc. in a non-blocking way.

🦖 How does React Suspense Work? 🦇

Basically, you may ask what is a Suspense. While there can be many functional definitions for it. The way I like to think about it is Suspense is a mechanism that can handle async calls (promises) in React render cycles.

As you all know, React rendering is (was actually) a synchronous step. And a renderer can work with only a VirtualDOM presented to it. It doesn’t know how to handle things like promises which can resolve() or error out in future. In order for React to be able to handle promises, which can be anywhere deep in the VirtualDOM we are trying to build, we needed a way to signal our DOM creation process that we need to wait for it and replace that part with something like a fallback to show to the UI.

And, also keep track of all such promises in-flight where ever they might be, so that once resolved we can automatically correct our VirtualDOM and trigger a render.

🐙 What is Concurrent Mode 🦑

The best way to handle these cases which are not always parent-child relationships is through our classic JavaScript try().. catch() block, which can short circuit and execution flow for us anytime. React team took this controversial decision to throw promises from the render tree (VirtualDOM) in order to signal something is still loading.

While this is not encouraged by the react team for you to learn more on these low-level details as they can be misused or even be out of sync if you are building a library on top of it. You won’t find them on their docs. Here, is what the official documentation has to say about it,

Avoid using low-level details on using React

But, if you are a vivid code reader or ponder around these details, there are plenty of places where you can find more details about them, e.g.

Concurrent Mode is more important than just typical implementation detail — it’s a foundational update to React’s core rendering model. So while it’s not super important to know how concurrency works, it may be worth knowing what it is at a high level.

A key property of Concurrent React is that rendering is interruptible. In previous versions of React — rendering in a single, uninterrupted, synchronous transaction. With synchronous rendering, once an update starts rendering, nothing can interrupt it until the user can see the result on the screen.

In a concurrent render, this is not always the case. React may start rendering an update, pause in the middle, then continue later. It may even abandon an in-progress render altogether.

Fundamentally, concurrent Mode fixes this basic limitation by making the rendering process interruptible.

Alright, as promised earlier, we will try to see and implement them in order to appreciate the clever mechanisms used to help you understand these even in further detail with our own React lib we have been building so far. Without any further due, let’s get on with it.

🐸 Setting things up 🐣

As usual setup things up, we will start where we left the code from the previous section. You can get started by forking the same on Stackblitz. Or as with other parts, you can DIY with your own typescript setup as long as you can follow along with the same/similar code.

Out starting point for React Suspense

🦈 Our own little remote API 🐋

We will first, set the goals for the rest of the post, basically, out of many use cases Suspense/Concurrent Mode solves, we will pick one use-case (I/O bound) of fetching some remote data/resource and displaying it on the UI without explicit state management.

In fact, for the fun of it, let me fetch an image resource directly and show it on the UI may be. While we can use all the free APIs available, just to make this post full proof for the future as well, we will simulate a remote API call in our code itself which when called returns an image URL after some setTimeouttime. Let’s see the code for the same.

// ---- Remote API ---- //
const photoURL = 'https://picsum.photos/200';
const getMyAwesomePic = () => {
return new Promise((resolve, reject) => {
setTimeout(() => resolve(photoURL), 1500);
});
};

Simply put, when getMyAwesomePicis called, this will closely simulate a remote API call and getMyAwesomePic will resolve after 1.5 secs to return photoURL which we intend to render in our App.

Let’s see what happens when we try to use this in our App just like that.

// ---- Application --- //
const App = () => {
const [name, setName] = useState('Arindam');
const [count, setCount] = useState(0);
const photo = getMyAwesomePic();return (
<div draggable>
<h2>Hello {name}!</h2>
<p>I am a pargraph</p>
<input
type="text"
value={name}
onchange={(e) => setName(e.target.value)}
/>
<h2> Counter value: {count}</h2>
<button onclick={() => setCount(count + 1)}>+1</button>
<button onclick={() => setCount(count - 1)}>-1</button>
<h2>Our Photo Album</h2>
<img src={photo} alt="Photo" />
</div>
);
};

And our results will be as expected, photo will simply be a promise object which hasn’t been resolved yet but our rendering is finished and no one is waiting for it to be reflected on the UI.

promise can’t be rendered as-is

Next, we will create our main Suspense wrapper to handle such promises within our components.

🌳 Suspense and Caching mechanisms with 🌴 createResource

One important thing to note here is that we don’t want our application owners to worry about this resource loading and its management and our neat little library should do the heavy lifting.

So, the way we can get a handle on such cases and do our own little try()..catch() magic on them is to provide a library API just like we did with useState for managing states. Let’s call this API createResource() , this will also provide an internal caching mechanism to keep track of all promises inflight and things which has been resolved. At a high level, we will also need a key that will be used by the createResource() to keep track of specific Async calls being made which can be referred to later. So the interface for createResource() will look something like createResouce(asyncTask, key) . Let’s try to implement it, at a high level here,

// ---- Library ---- //
const resourceCache = {};
const createResource = (asyncTask, key) => {
// First check if the key is present in the cache.
// if so simply return the cached value.
if(resourceCache[key]) return resourceCache[key];
// If not then we need handle the promise here
....
}

Ok, we start off by creating a simple cache resourceCache = {} for all our async tasks. Then, first, we check if the resourceCache already has a value for the specified key (which essentially will mean the task is done and its value is present in the cache) we simply return the same and we are done, we don’t need to think about it. But, in case it's a fresh request or we don’t have the result in our cache, then we need to think about what can be done.

But, fundamentally, we should not continue rendering at this point in the normal flow as we know this value which is not resolved yet wherever used on the UI will not work at this point. So, let’s see what can be done to achieve it.

💫 Branching our VirtualDOM creation 🐲

This is where we will introduce the key idea of throwing a promise to our VirtualDOM to indicate that the current branch of execution can not create the whole VirtualDOM we intend to render and should branch off creating a fallback VirtualDOM that can be rendered immediately. Let’s implement that in the code.

// ---- Library ---- //
const resourceCache = {};
const createResource = (asyncTask, key) => {
// First check if the key is present in the cache.
// if so simply return the cached value.
if (resourceCache[key]) return resourceCache[key];
// If not
throw { promise: asyncTask(), key };
};

The above code now breaks our whole VirtualDOM creation process abruptly, let’s see when we try to use this what happens on the App.

// ---- Application --- //
const App = () => {
const [name, setName] = useState('Arindam');
const [count, setCount] = useState(0);
const photo = createResource(getMyAwesomePic, 'photo');return (
<div draggable>
<h2>Hello {name}!</h2>
<p>I am a pargraph</p>
<input
type="text"
value={name}
onchange={(e) => setName(e.target.value)}
/>
<h2> Counter value: {count}</h2>
<button onclick={() => setCount(count + 1)}>+1</button>
<button onclick={() => setCount(count - 1)}>-1</button>
<h2>Our Photo Album</h2>
<img src={photo} alt="Photo" />
</div>
);
};

And sure enough, our UI breaks completely on this line(createResource) as we are not handling any errors that can be thrown from a React Component (App in our case), which results in an Uncaught Exception.

So, let’s do that next. Just throwing the promise is not enough from the Component, we should also be able to catch it at our top VirtualDOM creation process (React.createElement) so that we can handle it later once the data is ready. Let’s modify our createElement to account for this case.

// ---- Library --- //
const React = {
createElement: (tag, props, ...children) => {
if (typeof tag === 'function') {
try {
return tag(props, ...children);
} catch ({ promise, key }) {
console.log(promise);
console.log(key);
}

}
const el = {
tag,
props,
children,
};
return el;
},
};

With this change, we are able to catch() this specific error and log them. But remember, we can’t continue with the current VirtualDOM creation as the actual value is still Promise(<pening>) which our React doesn’t know how to render. Let’s see the output here,

Still, we are now in a much better position as we can branch off from the catch block and provide our createElement with the fallback UI. For this demo, I have simply created a single h2 element saying loading your image as a fallback.

// ---- Library --- //
const React = {
createElement: (tag, props, ...children) => {
if (typeof tag === 'function') {
try {
return tag(props, ...children);
} catch ({ promise, key }) {
// We branch off the VirtualDOM here
// now this will be immediately be rendered.
return { tag: 'h2', props: null, children: ['loading your image'] };

}
}
const el = {
tag,
props,
children,
};
return el;
},
};

Sure enough, we get the fallback UI, immediately and the render() doesn’t complain anymore because our returned VirtualDOM (fallback) can be rendered without issues.

fallback UI on the try..catch case

☃️ Suspense in Action 💨

But we are not done yet. Right, we need to be able to handle the promise when it's resolved. How do we go about handling them, let’s first see what we want to be done once its resolved,

  • We want to populate the resourceCache with the key and the value returned from the Promise.
  • We want to trigger a reRender() as we know this time for this resource we have bothkey and value. it will be a cache hit (we just put it there) and createResource() won’t throw any exception anymore it will simply return the result from the cache.
  • Automatically, our App can now fully render() as we have resolved all values needed in the VirtualDOM.

Let’s code this last bit to see everything in action.

// ---- Library --- //
const React = {
createElement: (tag, props, ...children) => {
if (typeof tag === 'function') {
try {
return tag(props, ...children);
} catch ({ promise, key }) {
// Handle when this promise is resolved/rejected.
promise.then((value) => {
resourceCache[key] = value;
reRender();
});

// We branch off the VirtualDOM here
// now this will be immediately be rendered.
return { tag: 'h2', props: null, children: ['loading your image'] };
}
}
const el = {
tag,
props,
children,
};
return el;
},
};

And Voila! You have your App which loads and detects it has a remote API call, shows the fallback UI for the duration its remote API call is being made and once the API call resolves, shows the entire UI altogether without you as an application developer not worrying about any of it.

React Suspense in Action

🥝 Conclusion 🥥

As always, if you have this far Congratulations! This is at the heart of React Suspense and Concurrent Mode. Now you are a React Pro inside out :)

Now, for this post, I wanted to cover truly parallel rendering as well. But looking at the length of this post, I will leave it as an exercise for you. What I mean by this is that ourApp even if it does everything you would expect it to, the way we are completely throwing away current VirtualDOM rendering fallback as soon as we get an exception, is a little counterproductive when you are loading multiple resources. Because it will go through the same fallback and reRender cycles for each such resource loading one after another, in a sequential manner, let me illustrate that with this example,

// ---- Application --- //
const App = () => {
const [name, setName] = useState('Arindam');
const [count, setCount] = useState(0);
const photo1 = createResource(getMyAwesomePic, 'photo1');
const photo2 = createResource(getMyAwesomePic, 'photo2');
return (
<div draggable>
<h2>Hello {name}!</h2>
<p>I am a pargraph</p>
<input
type="text"
value={name}
onchange={(e) => setName(e.target.value)}
/>
<h2> Counter value: {count}</h2>
<button onclick={() => setCount(count + 1)}>+1</button>
<button onclick={() => setCount(count - 1)}>-1</button>
<h2>Our Photo Album</h2>
<img src={photo1} alt="Photo1" />
<img src={photo2} alt="Photo2" />

</div>
);
};

What will happen here is, it will load the photo1 (first throw) first showing that fallback UI, when that is resolved, it will reRender that’s when it will hit the second createResource which is not resolved so it will throw again the fallback UI showing for photo2 and once that is resolved it will trigger the final reRender() which then loads the UI with both the images. Let’s see it in action to get what I mean,

Multiple Resources are loaded Sequentially

Now, you can find the code for this specific problem here. Try to solve this by making both of them parallel and still being able to render once both are loaded successfully. It should be fun!!

Let me know in the comments if you need help with that. Happy Coding!

Code for this Part of the Series can be found here.

--

--