Async React with Suspense
Tags: react, suspense, javascript, beginners
Published: 2024-06-02 08:02:53


As React devs we deal with async stuff every day. We know firsthand how awkward and complicated async logic can get.

Now add components into the mix it only gets more complicated.

Suspense was the solution that React came up with to solve for that. Announcement of Suspense happened in 2018. It took a while to stabilize. Finally, with React 19, we will have all the pieces to use it.

The most fascinating thing about Suspense was the use of throw. Yes, you read it right!

Suspense uses throw statements,
intended for error handling, as a control flow mechanism.

Yup, 🤯.

Before we dive into the heavy stuff, let's review what `async` JS is:

### Async JavaScript

Let's take this async function:

```typescript
function fetchText({ text, delay }: DelayText): Promise<string> {  
  return new Promise<string>((resolve) =>  
	setTimeout(() => {  
	  resolve(text);  
	}, delay),  
  );  
}
```

It takes in two parameters:

* a text — which will be returned after a delay
    
* a delay — a specified duration after which the text is returned
    

How would we go about consuming such a function?

Variant 1:

```typescript
import { fetchText } from "./sample.ts";  
  
async function getTexts(): Promise<[string, string]> {  
  const text1 = await fetchText({ text: "hello 1", delay: 1000 });  
  const text2 = await fetchText({ text: "hello 2", delay: 2000 });  
  return [text1, text2];  
}  
  
console.time("gettingTexts");  
getTexts().then((res) => {  
  console.log({ res });  
  console.timeEnd("gettingTexts");  
});
```

If you run this on say `node`, you should see an output like this:

```js
{
  res: [ "hello 1", "hello 2" ]
}
[3.01s] gettingTexts
```

If you notice here, I have used `console.time` that prints out the time taken to execute.

Which in the above case is approximately 3 seconds, i.e., (1 sec for `text1`) + (2 seconds for `text2`)

Can we do better?

Variant 2:

```ts
import { fetchText } from "./sample.ts";  
  
async function getTexts(): Promise<[string, string]> {  
  const text1 = fetchText({ text: "hello 1", delay: 1000 });  
  const text2 = fetchText({ text: "hello 2", delay: 2000 });  
  return [await text1, await text2];  
}  
  
console.time("gettingTexts");  
getTexts().then((res) => {  
  console.log({ res });  
  console.timeEnd("gettingTexts");  
});
```

By making a small change i.e., moving `await` to the bottom, we have already improved the runtime of the above script:

```js
{
  res: [ "hello 1", "hello 2" ]
}
[2.01s] gettingTexts
```

Now it takes 2 seconds! i.e., instead of `time of request 1 + time of request 2` it has become `max (time of request 1, time of request 2)`

Ain't that pretty cool?

This is the essence of `concurrent` programming. We dispatch requests at the same time and wait for them to finish.

So instead of running one thing at a time, i.e., synchronously. We are running things concurrently and achieving net better performance.

Picture worth a thousand words.


![concurrent requests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g1nxx062opr0wl8jed4c.png)


Okay, now what about error handling?

We have good old `try/catch`.

We just have to wrap the `async` code in it :

```ts
async function getTexts(): Promise<[string, string]> {  
  try {  
    const text1 = fetchText({ text: "hello 1", delay: 1000 });  
    const text2 = fetchText({ text: "hello 2", delay: 2000 });  
    return [await text1, await text2];  
  } catch (e) {  
    console.error("Error fetching texts");  
    return ["error fetching text 1", "error fetching text 2"];  
  }  
}
```

Now that we have gotten an understanding of what async JS is, let's dive into Async React:

## Suspend React

Up until now, we dealt with `async` resources directly. But what about components that have a `async` dependency?

For example, let's take the same `fetchText` function. Instead of logging that value on the console, I want to show it in UI.

The common way of doing this in `React` is via `useEffect` (Variant 1):

```javascript
import { useEffect, useState } from "react";  
import { fetchText } from "../async/sample.ts";  
import { Card } from "./Card/Card.tsx";  
  
function EffectCard() {  
    const [text, setText] = useState("loading..");  
    useEffect(() => {  
        fetchText({ text: "hello, world!", delay: 1000 })  
            .then((res) => {  
                setText(res);  
            })  
            .catch(() => {  
                setText("error");  
            });  
    }, []);  
    return <Card text={text} />;  
}
```

### Sidenote

For more info on why this may not be a good idea, please read this post by core contributor from `react-query` :

{% embed https://tkdodo.eu/blog/why-you-want-react-query %} 

Despite looking straightforward, `useEffect` has too many gotchas. Using it properly is challenging. I would recommend using a library which does it for you and `react-query` is a great option.

Now, this looks fine and well, but there is a problem with this.

If you think about it, this is similar to what we had in `Variant 1` for `async` handling:

```javascript
    const text1 = await fetchText({ text: "hello 1", delay: 1000 });  
    const text2 = await fetchText({ text: "hello 2", delay: 2000 });
```

Why so?

Because, `useEffect` runs after the render is completed.

So we are running things like this:


![sequential requests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t0b7dedgq84arkivjoiu.png)


This is `sequential` instead of `concurrent`. You — ***fetch after render.***

Let's see how we can improve this (Variant 2):

```typescript

// move promise outside of React!  
const helloTextPromise = fetchText({ text: "hello, world!", delay: 1000 });  

function EffectCard() {  
    const [text, setText] = useState("loading..");  
    useEffect(() => {  
        helloTextPromise  
            .then((res) => {  
                setText(res);  
            })  
            .catch(() => {  
                setText("error");  
            });  
    }, []);  
    return <Card text={text} />;  
}
```

With this small change, we have them running concurrently!


![concurrent rendering](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1vmkgvl9v0tyrlz5ersl.png)


Now that is much better. It's easy to get caught up in the framework/library and forget we are still using vanilla JS 😀

Okay, let's extend the above example (Variant 3):

```typescript
import { EffectCard } from "./EffectCard.tsx";

function Input() {  
    const [name, setName] = useState("");  
    return (  
        <input  
            name={"userName"}  
            value={name}  
            onChange={(e) => setName(e.target.value)}  
        />  
    );  
}  
function EffectAndInput() {  
    return (  
        <div>  
            <Input />  
            <EffectCard />  {/*Same as above*/}
        </div>  
    );  
}
```

Now we have two components loading instead of one. One is the`EffectComponent` other is an input text element.

The effect can be slow or fast. Since we are emulating things here, let's make it slow.

This means that while the async stuff is still running if the user tries to type, it can create a janky experience. I am sure everyone has experienced this at one time or another.

When we try to type something but nothing shows up after a few moments it shows up at once.

This is because of this.

Here we are trying to unblock UI. i.e., we don't want to “await” on something that can take a lot of time to run. In this case, it's a component.

How can we let React know this?

### Enter Suspense.

To signal React that this component is loading something that will take some time.

As I mentioned at the beginning we need to:

`throw promise`

Yup 🤯 (it still does).

By throwing a promise from a component, you let React know that there is some async activity going on. So React will suspend the component.

React can do other things while suspending the component!

This is ***Async React***!

Let's refactor the above example to use Suspense.

We need to add a little logic to stop throwing the `promise` when it's finished. For that, we have this small utility:

```typescript
type Status = "Loading" | "Done" | "Error";  
  
function createResource<T>(promise: Promise<T>) {  
    let status: Status = "Loading";  
    let result: T | null = null;  
    let error: Error | null = null;  
    promise  
        .then((res) => {  
            status = "Done";  
            result = res;  
        })  
        .catch((err) => {  
            status = "Error";  
            error = err;  
        });  
    return {  
        read(): T {  
            switch (status) {  
                case "Loading":  
                    throw promise; /* throw until promise is still running */  
                case "Error":  
                    throw error;  
                case "Done":  
                    return result!;  /* return once done */
                default: {  
                    return status;  
                }  
            }  
        },  
    };  
}
```

As you can see, we are `throwing` the `promise` till it's in `Loading` state and returning the result once it's done.

With this, let us convert the `EffectCard` to a `SuspendedCard`:

```typescript
import { fetchText } from "../async/sample.ts";  
import { Card } from "./Card/Card.tsx";  
import { createResource } from "../async/create-resource.ts";  
  
// Same as before - start the promise outside of React life cycle
const helloTextResource = createResource(  
    fetchText({ text: "hello, world!", delay: 1000 }),  
);  
function SuspendedCard() {  
    const helloText = helloTextResource.read();  
    return <Card text={helloText}></Card>;  
}
```

⚠️ Don't create the resource inside the `Component`! Because that would mean that a new resource will be created each time the component re-renders. Which is not something we wish to have (Infinite loops!).

Keep the components `idempotent`.

Now let's refactor `Variant 3` to use the `SuspendedCard`:

```typescript

import { Suspense } from "react";  
import { SuspendedCard } from "./SuspendedCard.tsx";  
import { Input } from "./Input.tsx";  
  
function SuspendedEffectAndInput() {  
    return (  
        <Suspense fallback={<h1>loading..</h1>}>  
            <Input />  
            <SuspendedCard />  
        </Suspense>  
    );  
}  
  
export { SuspendedEffectAndInput };
```

Now isn't this cool?

Few things to note here though:

* The `Suspense` the wrapper is called the `Suspense Boundary`. This is where you provide a `fallback` i.e., while the component is suspended you can show something else. A loading indicator, for example.
    
* The Suspense boundary is granular. You can put it anywhere you like. Directly above the `<SuspendedCard />` or like I have done in the snippet.
    

The Suspense boundary is so flexible because we are literally ***throwing*** the ***promise***!

When you throw something, we can have any number of layers above. We handle it wherever we wish. Now throwing stuff other than errors makes sense. Also, we must handle it!

What about errors? 🤔

If you had noticed in the `createResouce` function, we had this case too:

```ts
case "Loading":  
    throw promise;  
case "Error":  
    throw error; /* error scenario */  
case "Done":  
    return result!;  
default: {  
    return status;
```

So, in case of an error, we throw the error instead of the original promise.

To handle it, we need to wrap it in a `ErrorBoundary`:

```typescript
import { Suspense } from "react";  
import { SuspendedCard } from "./SuspendedCard.tsx";  
import { Input } from "./Input.tsx";  
import { ErrorBoundary } from "react-error-boundary";  /* you have to install this library */
  
function SuspendedEffectAndInput() {  
    return (  
        <ErrorBoundary fallback={<h1>Some error occurred!</h1>}>  
            <Suspense fallback={<h1>loading..</h1>}>  
                <Input />  
                <SuspendedCard />  
            </Suspense>  
        </ErrorBoundary>  
    );  
}  
  
export { SuspendedEffectAndInput };
```

This is similar to wrapping it with `try/catch` block!

This whole approach is called `render as you fetch`

Let's push this a bit further.

We can even load the component code `dynamically` via `lazy` .

So that means that both `code` and `data` can be loaded concurrently!

Let's see how that can be accomplished:

```typescript

import { lazy, Suspense } from "react";  
import { Input } from "./Input.tsx";  
import { ErrorBoundary } from "react-error-boundary";  
import { createResource } from "../async/create-resource.ts";  
import { fetchText } from "../async/sample.ts";  
const Card = lazy(() =>  
    import("./Card/Card.tsx").then((mod) => ({ default: mod.Card })),  
);  /* async logic to get code */ 
  
const helloTextResource = createResource(  
    fetchText({ text: "hello, world!", delay: 1000 }),  
); /* async logic to get data */ 

function SuspendedResourceCard({  
    resource: { read },  
}: {  
    resource: { read: () => string };  
}) {  
    const helloText = read();  
    return <Card text={helloText}></Card>;  
}  
  
function FinalVariant() {  
    return (  
        <ErrorBoundary fallback={<h1>Some error occurred!</h1>}>  
            <Suspense fallback={<h1>loading..</h1>}>  
                <Input />  
                <SuspendedResourceCard resource={helloTextResource} />  
            </Suspense>  
        </ErrorBoundary>  
    );  
}
```

As you can see, we are loading both code and data concurrently!

Suspense boundaries can handle both of them together without us having to manage the low-level details. The best part is React can do other things like update UI when the user types on the input (no more jank!)

Another cool thing is now with `RSC` we have a way to start loading `promises` on the server and then `Suspend` them on the client!

Some additional resources if you want to deeper into this:
{% embed https://overreacted.io/algebraic-effects-for-the-rest-of-us/ %}

This `Youtube` video by Jack details on how to leverage these things on the server:

{% embed https://www.youtube.com/watch?v=ViVa5JPGrf4 %}

Also, we don't have to use the `createResource` method. The React team is shipping with a new `hook` in `React 19` called `use` :



For a live version of all the code samples you can check them out here:

{% embed https://github.com/varenya/react-suspense-blog %}

All right folks that's it, I hope this was helpful!