React unstable_deferredUpdates

June 1, 2018

On React's New unstable_deferredUpdates

react-dom recently added a new export, unstable_deferredUpdates. The source code can be found in react-reconciler: https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberScheduler.js#L1399

deferredUpdates let's you defer a low-priority setState call until later.

Culling unnecessary setState calls is an easy way to improve performance in a react app. Excessive setState calls will flog the render cycle and lead to unnecessary re-renders.

In source code it's most common to see setState called with a simple key/value pair:

setState({ loading: true })

setState Accepts a Function

setState also accepts a function. The function is called with the current state.

setState(state => {
  return { loading: true }
})

Returning Null Inside setState() Avoids an Update

If null is returned, then the tree is not re-rendered:

setState(state => {
  if (state.loading) {
    // no work performed
    return null
  }
  return { loading: true }
})

Functional setState is particularly useful in the case of deferredUpdates.

Because the update is deferred until later, this.state may be stale. Actually, this.state may also be stale. But with deferredUpdates this effect is more pronounced. Functional setState guards against a stale state.

load = () => {
  deferredUpdates(() => {
    this.setState(currentState => {
      // this.state !== currentState
      // this.state may be stale
      if (currentState.loading) {
        // noop
        return null
      }
      return { loading: true }
    })
  })
}

Cancellable deferredUpdates

deferredUpdates are also cancellable. For example, you may be familiar with the error:

Can't call setState (or forceUpdate) on an unmounted component

This can be avoided by cancelling a deferred update to state by returning null inside setState.

import React from 'react'
import { unstable_deferredUpdates as deferredUpdates } from 'react-dom'
class App extends React.Component {
  state = {
    data: undefined,
    loading: true
  }
  isCancelled = false
  fetchDataFromThirdPartyApi = async () => {
    let resp = await fetch('https://api.ipify.org?format=json')
    let data = await resp.json()
    deferredUpdates(() => {
      this.setState(state => {
         if (isCancelled) {
           // noop
           return null
         }
         return { data, loading: false }
      })
    })
  }
 
  componentDidMount () {
    this.fetchDataFromThirdPartyApi()    
  }
  
  componentWillUnmount () {
    this.isCancelled = true
  }
}

Use Cases for deferredUpdates: Repeated Calls to setState

Suppose you want to make a request to a third-party API, but show a spinner while the request is in-flight.

You might do something like this:

load = async () => {
  this.setState({ loading: true })
  let data = await this.fetchDataFromThirdPartyApi()
  this.setState({ loading: false, data: true })
}

This has a few problems. setState is asynchronous, so we'll be requesting data before this.state.loading is updated. We could use the second, callback argument of setState to only start fetching data after state has been updated. However, it may be faster to call setState without awaiting the update to begin fetching data immediately.

With deferredUpdates:

load = async () => {
  // promisify setState
  await new Promise((resolve, reject) => {
    this.setState(prevState => {
      if (prevState.loading) {
        reject()
        return null
      }
      return { loading: true }
    }, resolve)
  })
  let data = await this.fetchDataFromThirdPartyApi()
  deferredUpdates(() => {
    this.setState(() => {
      // if the component is unmounted, avoid the update
      if (this._isUnmounted) return null
      return { loading: false, data }
    })
  })
 }
 
 componentWillUnmount () {
   this._isUnmounted = true
 }

One downside of the above snippet is it adds some code complexity and a simple setState({ [key]: value }) is more readable/scannable.

In the future, I hope the React team will expose more react internals to allow users to interact with the update queue.