Making Promises Suspendable
Tags: react, javascript, webdev, showdev
Published: 2023-03-20 13:35:41



![Making Promises Suspendable](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zcx1l31ia2e7lg2sxsqo.png)


This is a two-part article in which we focus first on understanding the foundations of the reasons to use the Suspense API, and in the next part, we will discuss how to use the [New Suspense hooks for Meteor](https://dev.to/grubba/new-suspense-hooks-for-meteor-3ddg)

I will explain how to make your promises go into suspense mode and what it takes to get there.

I recommend two articles for theory on this side that helped me understand how React deals with async values. One is from **[Dan Abramov](https://mobile.twitter.com/dan_abramov) [- Algebraic Effects for the Rest of Us](https://overreacted.io/algebraic-effects-for-the-rest-of-us/),** and the other is from  [Radosław Miernik](https://radekmie.dev/about/) - **[On `throw` in React](https://radekmie.dev/blog/on-throw-in-react/)**

## Why use suspense?

As this [good article](https://www.pluralsight.com/blog/software-development/suspense-react-18-explained#:~:text=Suspense%20is%20the%20first%20feature,intuitive%20API%20to%20work%20with) explains, Suspense gives us two great features: 

- Declarative code which means that React Suspense simplifies the process of writing UI code by allowing developers to declare what they want the UI to look like rather than writing code that tells the UI how to render step-by-step. One example of this feature is the removal of the use `isLoading` variable. By placing this variable above the UI code, developers can ensure that their code is in sync with the UI. This eliminates the need for awkward `if` statements.
- Better end-user experience with Suspense. You can use the newly added concurrent rendering engine in React. This Suspense feature gives the end user a better feeling of how the UI looks and acts because react only will update what it needs.

Theory aside, here is [my gist](https://gist.github.com/Grubba27/25fea366c118f459f7af5e3b9e1dcc78), which is why this article exists. You can use this [playground](https://playcode.io/1297205) as well.

Now I will explain how this works. First, you will need a cache map to store your promises. As React suspends, the promise and then fulfills it, you need a way to track this specific promise (this was a part that I struggled with a lot to understand, it has to be the exact same promise each time). If there is no value in the given key, the hook will create an entry in the map with this `promise` and `throw` it. Here is where the magic happens. The `suspensify` hook looks like the snippet below in its first run:

```tsx
function suspensify<T>(key: string, promise: Promise<T> | null, deps: DependencyList = [], lifespan = 0): T {
  const cached = cacheMap.get(key)
	**//    ˆˆˆ this is undefined. all code below is not ran
  /* ... code ...*/
 const entry: Entry = {
    deps,
    promise: new Promise((resolve, reject) => { // here you work in your promise / waitable value. remember to set the result and error
      promise
        .then((result) => {
          entry.result = result
          resolve(result)
        })
        .catch((error) => {
          entry.error = error
          reject(error)
        })
    })
  }
  
  cacheMap.set(key, entry)
  throw entry.promise
// ˆˆˆˆthis is where we jump into the fallback
}
```

Before we talk about the return side of the suspense API, I would like to point out that when you pass in a `null` value is the same as saying that you want it to return `null` and not run the hook, as we cannot put an `if` statement above a hook, this is the way to deal with control flow.

Well, when the hook runs again (yes, that is how suspense works), we will go to the `if` that was above the entry creation:

```tsx
function suspensify<T>(key: string, promise: Promise<T> | null, deps: DependencyList = [], lifespan = 0): T {
  const cached = cacheMap.get(key)
  //    ˆˆˆ now we have a value here
  useEffect(() =>
    () => {
      setTimeout(() => {
        if (cached !== undefined && isEqual(cached.deps, deps)) cacheMap.delete(key)
				// ˆˆˆˆclean when the hook unmounts.
      }, lifespan)
    }, [cached, key, ...deps])
  
  if (promise === null) return null // we talked about this one

  if (cached !== undefined) {
    if ('error' in cached) throw cached.error
														// ˆˆˆˆtrigger error boundary
    if ('result' in cached) {
      const result = cached.result as T
      setTimeout(() => {
        cacheMap.delete(key)
      }, lifespan)
      return result // resolve suspense
    }
    throw cached.promise // as we have not got any result or err we should wait a bit more
  }
  
/* ... code ...*/
}
```

There are some caveats. We need to address that `setTimeout` that works as “defer” they are necessary because after we return the value of the promise, we need to clean this key, as it may be required to run another code when this component mounts again. You can adjust the lifespan if you want to reuse the same value in other places but be careful not to live in old data, an excellent way to start is with 0 and then increase the lifespan of this `key` result.

And there you go, you have learned a bit about how Suspense works and how you can use it today to deal with your promises.

[React Query](https://tanstack.com/query/v4/docs/react/guides/suspense) already supports Suspense, and [Relay](https://relay.dev/docs/guided-tour/rendering/loading-states/) is our most outstanding example of Suspense API in practice.

If you got interested in what you saw here and want to see implemented versions of a library using the suspense API, you could check the next chapter of this article series, where is shown the [New Suspense hooks for Meteor](https://dev.to/grubba/new-suspense-hooks-for-meteor-3ddg)