Geromekevin

Updater Functions in React's setState

April 05, 2019 • ☕️ 4 min read

I recently learned a little nugget about how functions in setState work.

Now I begin to see … 💡

This article is for you if you are unaware of the differences between these lines (especially between 3 and 5):

this.setState({ on: !this.state.on });
// ,
this.setState(prevState => ({ ...prevState, on: !prevState.on }));
// , or
this.setState(({ on }) => ({ on: !on }));
// 🤔

I learned (and I’m still learning) React using video lectures. In most courses about React I watched the instructor updating state like this:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

const styles = {
  container: {
    fontFamily: 'sans-serif',
    display: 'flex',
    flexDirection: 'column',
    justifyContent: 'center',
    alignItems: 'center',
  },
};

class StateExample extends Component {
  state = { on: false };

  toggleLight = () => {
    this.setState({ on: !this.state.on });
  };

  render() {
    const { on } = this.state;
    return (
      <div style={styles.container}>
        The light is on {`The light is ${on ? 'on💡' : 'off 🌃'}`}
        <button onClick={this.toggleLight}>Toggle light</button>
      </div>
    );
  }
}

const rootElement = document.getElementById('root');
ReactDOM.render(<StateExample />, rootElement);

You can copy and paste this code to CodeSandBox. Notice how setState gets an object as its parameter. This is okay for most state. However, in this example the next value of this.state.on depends on the current value of this.state.on.

The recommended way of updating state in React based on previous state is using updater functions instead of objects. I first read this in “Using a function in setState instead of an object” by Sophia, which is a great article 👏🏻.

Consequently, for months I’ve been updating state which depends on previous state, using a function and the prevState parameter.

this.setState(prevState => ({ ...prevState, on: !prevState.on }));

This always worked for me and I haven’t run into any problems. Then I watched Kent C. Dodds using a function in setState in conjunction with destructuring.

this.setState(({ on }) => ({ on: !on }));

What is the different between these two? I experimented with both, and I couldn’t discover a difference.

It turns out it comes down to the inner workings of React. setState calls another function called enqueueSetState.

// In react/src/ReactBaseClasses.js
Component.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  // Here enqueueSetState is called.
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

// In react/packages/react-test-renderer/src/ReactShallowRenderer.js
enqueueSetState(publicInstance, partialState, callback, callerName) {
  this._enqueueCallback(callback, publicInstance);
  const currentState = this._renderer._newState || publicInstance.state;

  if (typeof partialState === 'function') {
    partialState = partialState.call(
      publicInstance,
      currentState,
      publicInstance.props,
    );
  }

  // Null and undefined are treated as no-ops.
  if (partialState === null || partialState === undefined) {
    return;
  }

  this._renderer._newState = {
    ...currentState,
    ...partialState,
  };

  this._renderer.render(this._renderer._element, this._renderer._context);
}

As you can see here, currentState and partialState are being spread out. Or as the docs put it:

“State Updates are Merged”

It follows that using prevState always results in

state = {
  ...prevState,
  ...{ ...prevState, expanded: !prevState.expanded },
};

, whereas Kent’s way of destructuring state results in

state = { ...prevState, { expanded: !expanded } };

, which is arguably better.

One more thing: You can see the benefits of destructuring the state even more if you have nested objects in your state.

state = { nested: { on: false, hangingFromCeiling: true } };

this.setState(({ nested }) = ({ nested: { ...nested, on: !on } }));

Here we keep our light bulb hanging from the ceiling by destructuring and spreading out the nested key.

I hope you learned something interesting here today 🎓.