# Building a Polyfill for React Suspense

The best way to understand a software concept is to build it yourself. Let's build a simplified version of React Suspense!

Canonical URL: https://www.transitivebullsh.it/building-a-polyfill-for-react-suspense

Author: Travis Fischer

Published: 2018-08-31

Updated: 2026-08-24T07:18:00.000Z

Tags: React.js

![Building a Polyfill for React Suspense](<https://assets.cultural-alignment.com/personal-site/media/3f6145a6dd54f377a406da0ee94a9e7647b0aca84bdbec700b571111d95a9b04.jpg>)

<a id="4d6eb3716a974bc499aca6ab6bc53177"></a>

## Intro

If you love React, you’ve probably heard something about the [upcoming Suspense APIs](<https://github.com/sw-yx/fresh-async-react>), but even after watching a [demo](<https://github.com/acdlite/suspense-ssr-demo>) or [two](<https://slides.com/swyx/react-suspense#/>), it was pretty difficult for me to lay my finger on how **exactly** Suspense works.

So I put my computer science cap on and decided to try and recreate it with the current version of React v16.

A few disclaimers before we get started that my fictional legal team wants to get out of the way.

The actual version of Suspense that will ship with React is [significantly](<https://github.com/facebook/react/pull/12279>) [more](<https://github.com/facebook/react/pull/13397>) [complicated](<https://github.com/facebook/react/pull/13398>) and efficient than the version in this polyfill. This tutorial & accompanying module are meant mainly for learning and experimental purposes. Also, the current polyfill will likely not play well with SSR.

[**Hic Dracones!**](<https://en.wikipedia.org/wiki/Here_be_dragons>)

> If you only care about the codes, check out react-suspense-polyfill, otherwise here we go!

<a id="cd7869a48267463e888bc6bdbca128e8"></a>

### **Setting the Stage**

IMHO, Suspense is a very powerful addition to the core React API surface, and I believe it will have a profound effect on how pragmatic React code is written a few years from now.

If you take nothing else away from this article, understand this:

> At its core, React Suspense works by allowing an async component to throw a Promise from its render method.

This polyfill mimics React’s internal support for this behavior by implementing an [error boundary](<https://github.com/transitive-bullshit/react-suspense-polyfill/blob/master/src/timeout.js#L24>) in the [**Timeout**](<https://github.com/transitive-bullshit/react-suspense-polyfill/blob/master/src/timeout.js>) component. If the error boundary encounters a thrown Promise, it waits until that Promise resolves and then attempts to re-render its children. It also handles falling back to loading content if the Promise takes too long to resolve. (explained in detail below)

I hope this module and accompanying demos make it easier to get up-to-speed with React Suspense. 😄

<a id="ae54482d49114ecbb8832cf905642aea"></a>

## **React.Suspense**

```javascript
import React from 'react'
import PropTypes from 'prop-types'
import Timeout from './timeout'

export default function Suspense (props) {
  const {
    delayMs,
    fallback,
    suspense,
    children
  } = props

  return (
    <Timeout ms={delayMs} suspense={suspense}>
      {didExpire => (didExpire ? fallback : children)}
    </Timeout>
  )
}

Suspense.propTypes = {
  delayMs: PropTypes.number,
  fallback: PropTypes.node,
  suspense: PropTypes.node,
  children: PropTypes.node
}

Suspense.defaultProps = {
  fallback: null,
  suspense: null,
  children: null
}
```

**Suspense** is the main public-facing component exposed by React Suspense. Its interface is relatively straightforward, exposing the following props:

- ` delayMs ` \- Amount of time in milliseconds to wait before displaying fallback / loading content. The main reason for adding a delay before displaying fallback content is to prevent loading indicators flashing too quickly before the main async content loads which can be an annoying UI distraction.
- ` fallback ` \- A React Node that will be displayed while any child component is loading *only* *after* ` delayMs ` have elapsed. This will typically be some type of loading spinner.
- ` suspense ` \- A React Node that will be displayed while any child component is loading *only* *before* ` delayMs ` have elapsed. Note: this optional prop is specific to react-suspense-polyfill and is strictly for the purpose of demoing how suspense works.
- ` children ` \- A React Node that represents the main content of this Suspense component which may or may not throw a Promise while loading asynchronous resources. See [react-async-elements](<https://github.com/palmerhq/react-async-elements>) for some examples of super sexy, async-friendly child components.

Note that in a previous internal version of React, the **Suspense** component was called **Placeholder**.

**Suspense** is the component you’re most likely to use in your code, but in the spirit of understanding how it works, the majority of the complexity is handled by **Timeout**.

<a id="61ffff05f5a349d2820b9afabea41fed"></a>

## React.Timeout

```javascript
import { Component } from 'react'
import PropTypes from 'prop-types'

export default class Timeout extends Component {
  static propTypes = {
    ms: PropTypes.number,
    suspense: PropTypes.node,
    children: PropTypes.func.isRequired
  }

  static defaultProps = {
    ms: 0,
    suspense: null
  }

  state = {
    inSuspense: false,
    didExpire: false
  }

  _expireTimeout = null
  _suspender = null

  componentDidCatch(err, info) {
    if (typeof err.then === 'function') {
      const suspender = err
      this._suspender = suspender
      this._initTimeout()
      this.setState({ inSuspense: true })

      const update = () => {
        if (this._suspender !== suspender) return
        this.setState({ inSuspense: false })
        this._clearTimeout()

        if (this.state.didExpire) {
          this.setState({ didExpire: false })
        } else {
          this.forceUpdate()
        }
      }

      suspender.then(update, update)
    } else {
      // rethrow non-promise errors
      throw err
    }
  }

  render() {
    const {
      children,
      suspense
    } = this.props

    const {
      inSuspense,
      didExpire
    } = this.state

    if (inSuspense && !didExpire) {
      // optional: strictly for the purpose of demoing how suspense works
      return suspense
    } else {
      return children(didExpire)
    }
  }

  _initTimeout() {
    const {
      ms
    } = this.props

    this._clearTimeout()

    this._expireTimeout = setTimeout(() => {
      this.setState({ didExpire: true })
    }, ms)
  }

  _clearTimeout() {
    if (this._expireTimeout) {
      clearTimeout(this._expireTimeout)
      this._expireTimeout = null
    }
  }
}
```

The **Timeout** component is a bit more tricky, so let’s break down what’s going on in steps:

1. The ` render ` method (Line 50) will initially invoke its ` children ` render function with a boolean value signifying whether or not this component has hit its timeout ` ms ` since mounting and encountering an async workload.
1. If the ` children ` render successfully, all is well with the world and React continues on as normal. 😃
1. If any component within the ` children ` subtree **throws a Promise** from its ` render ` method, it will be caught by Timeout’s error boundary, ` componentDidCatch ` (Line 24).
1. The error handler first starts a timeout for this async work (Line 28), such that the Timeout will fall back to displaying loading content if & when the ` ms ` timeout expires.
1. During the ` ms ` time before this Promise may expire, the ` Timeout ` is **“in suspense”** (Lines 29 and 63), which essentially means that we’re waiting for some resource to load but it hasn’t taken long enough to justify displaying the fallback / loading content just yet.
1. Once the Promise resolves (Line 43), Timeout once again invokes its ` children ` render prop (Line 39) with the expectation that this time, the initial asynchronous resource will resolve **synchronously** and all will once again be well with the world. 😃

Note that it’s entirely possible for a subtree to contain multiple, independent async resources, in which case the **Timeout** component may repeat steps 3–6 once for each async resource that needs to be resolved. Alternatively, **Suspense** & **Timeout** may be nested like any React component, so it’s entirely possible that a higher-level **Timeout** won’t need to handle an async request that is thrown lower in the React component tree if that request is captured by a Timeout closer to its origin. This follows the public behavior of React error boundaries pretty closely.

Hopefully, the **Suspense** and underlying **Timeout** components are now more concrete in terms of how they’re expected to behave.

99% of the time you’ll be working with a simple **Suspense** component and ignoring these details in **Timeout**, but I believe it’s extremely beneficial and empowering to have this type of deeper mental model to rely on for the type of fundamentally game-changing pattern that React Suspense supports.

And with that in mind, let’s talk a bit about how this basic mental model differs from the official version that the extremely talented React core team is cooking up!

![Subtrees upon subtrees upon trees. (Image Credit: Unsplash)](<https://assets.cultural-alignment.com/personal-site/media/fbe11e49a0e79c635625d648b0771df80daa397c4544d4055e41d0b2ca585c47.jpg>)

Subtrees upon subtrees upon trees. (Image Credit: Unsplash)



<a id="e719feb637014b21afcc98e4e071674d"></a>

## **Comparison to React Suspense**

There are two major limitations of this [polyfill](<https://github.com/transitive-bullshit/react-suspense-polyfill>) compared with the forthcoming official implementation of React Suspense.

<a id="aff09e2894da42fa9e5426b6d8ed380b"></a>

### **Correctness**

Okay, so we may have cheated a little bit 😉 There is one important detail that we left out of our implementation in terms of polyfilling the correct behavior.
Can you guess what it is?



---



If you’re not sure, that’s completely fine. I had done this whole coding exercise before I realized that [Dan Abramov](<https://medium.com/u/a3a8af6addc1>) had pointed out a potential flaw with this approach, so don’t worry if you’re drawing a blank…

The one potential correctness issue with this approach (that I’m aware of) is that React unmounts the **Timeout** subtree once an error is thrown, which has the unintended side effect of resetting all subtree components and their state each time an async resource is thrown or resolves.

React’s internal implementation of Suspense doesn’t suffer from this issue, as they have full control over tracking component state and can therefore ensure that partially rendered subtrees are properly restored after resolving suspenseful resources.

The good news here, however, is that this is very much an edge case, and empirically, I would expect that this doesn’t come into play very often. As long as you follow the 95% use case where the immediate child of **Suspense** is the only potentially async child component, and *that async child component eagerly loads all async state up front* instead of say, in response to user interaction, you won’t run into any problems. 👍

I’m actually curious if it would make sense for React core to enforce this restriction…

<a id="f70263d128e44018ac5278552e722a42"></a>

### **Efficiency**

This is the one area where a userland implementation of React Suspense simply can’t come close to the official core implementation. Otherwise, I’m sure the React team would’ve considered implementing this pattern on top of React as opposed to expanding the core React API surface.

In particular, the React team has done a lot of work in the past year or so to enable smarter re-use of partial rendering and the ability to suspend low priority updates in favor of higher priority updates that are closer to affecting a user’s perception of application responsiveness.

This work is collectively known as React Fiber, and React Suspense should be viewed as one of the first major optimizations that’s been enabled in React core as a direct result of the amazing foundation established with React Fiber.

Huge props to [Sebastian Markbåge](<https://medium.com/u/62e7de0d6312>), [Andrew Clark](<https://medium.com/u/6025bd347b9a>), [Dan Abramov](<https://medium.com/u/a3a8af6addc1>), [Sophie Alpert](<https://medium.com/u/b610e3f3fee2>), and the rest of the React team + contributors for their work in this area!

**Compatibility**
This polyfill does not currently support React ` v15 ` because error boundaries weren't properly supported until React ` v16 `. If you have ideas on how to add support for React ` v15 `, please submit an [issue](<https://github.com/transitive-bullshit/react-suspense-polyfill/issues>) and let's discuss!

Note that React will log an error to the console when using this polyfill regarding the thrown error, but *this console message can safely be ignored*. Unfortunately, there is no way to [disable](<https://github.com/facebook/react/issues/11098>) this error reporting for these types of intentional use cases. :sigh:

<a id="f34878654e814585949ceffa301f0ce6"></a>

## **Wrapping Up**

If you’ve read this far, please check out the [full source code](<https://github.com/transitive-bullshit/react-suspense-polyfill>) and ⭐️ the repo as a way of saying thanks!

I really hope you’ve found this article helpful. If you’re a React junkie, here are some related links:

- [Creating React Suspense in v16.2](<https://medium.com/@pete_gleeson/creating-suspense-in-react-16-2-dcf4cb1a683f>) \- Similar experiment by [Pete Gleeson](<https://medium.com/u/ba66bcf17eeb>).
- [react-suspense-starter](<https://github.com/palmerhq/react-suspense-starter>) \- Alternative which bundles a pre-built version of Suspense-enabled React allowing you to experiment with React Suspense right meow. By [Jared Palmer](<https://medium.com/u/fb7a3c353cc1>).
- [react-async-elements](<https://github.com/palmerhq/react-async-elements>) \- Suspense-friendly async React elements for common situations. By [Jared Palmer](<https://medium.com/u/fb7a3c353cc1>).
- [fresh-async-react](<https://github.com/sw-yx/fresh-async-react>) \- More Suspense stuff (code, demos, and discussions). By [Swyx](<https://medium.com/u/547f259e265e>).

Have any thoughts that I left out? Feel free to get in touch! ❤️
