Using this.setState() in React

Interactive forms

I was recently working on a form in React that looked something like this:

Shipping Address:
User Name
123 My Street
This Town, USA 45678

[ ] Use shipping address as billing address.

Billing Address:
User Name
456 Other Ave
That City, USA 56789

I wanted the form to have the following traits:

  • If the Use shipping address as billing address box is checked, copy over the data from the shipping address to the billing address
  • If there was previously data in the billing address and the user un-checks the box, bring back the old billing address

To accomplish these goals, I needed to implement some state management by using a class component and this.state. This is idiomatic React; once you understand how this.state works, you’d probably see this problem as trivial.

First approach

Here’s how you might instinctively use this.setState() in a class method on your component:

export default class OrderForm extends Component {

    constructor(props) {
        super();

        this.state = {
            selectedShippingAddress: null,
            savedBillingAddress: null,
            selectedBillingAddress: null,
            useShippingAsBilling: false,
        };

        this.handleToggleUseShippingAsBilling.bind(this);
    }

    // Version 1

    handleToggleUseShippingAsBilling() {

        const {
            useShippingAsBilling,
            selectedShippingAddress,
            savedBillingAddress,
        } = this.state;

        // If useShippingAsBilling is currently false, set it to `true`
        // and copy `selectedShippingAddress` to `selectedBillingAddress`

        if (!useShippingAsBilling) {
            return this.setState({
                useShippingAsBilling: true,
                selectedBillingAddress: selectedShippingAddress,
            });
        }

        // If useShippingAsBilling is currently true, set it to `false` 
        // and reset `selectedBillingAddress` to the `savedBillingAddress`

        return this.setState({
            useShippingAsBilling: false,
            selectedBillingAddress: savedBillingAddress,
        });
    }

    render() {
        // ... return JSX markup here
    }

As I found out by experience, when saving properties on this.state via this.setState(), React sometimes delays the operation:

Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.

This makes things a bit tricky. The handleToggleUseShippingAsBilling() method in Version 1 first reads the current value of this.state in order to determine what should happen next. If that happens before React has completed a previous this.setState() call, then we will end up with an undesired program state. We need to know that we are starting from the version of state that directly precedes the new state. Fortunately, there is a usage pattern for this.setState() that does exactly that. This is pretty clearly described in the React docs, but I always find that a concrete example helps me remember how things work.

Two kinds of this.setState()

You can use this.setState() in two ways:

  1. If you give it an object, it will overwrite this.state with that object’s properties. Let’s imagine we want to toggle a current state value called useShippingAsBilling. To use this form of this.setState(), we would read the value first, and then use this.setState to write the new value.
    const {
        useShippingAsBilling: prevValue, 
    } = this.state;
    
    this.setState({
        useShippingAsBilling: !prevValue;
    });
    
  2. If you give it a function, it will invoke the function and use its return value to overwrite this.state. The function signature will accept two arguments:
    1. The previous state value, or prevState
    2. The current props

These arguments are ensured by React to be up to date when the function is run. We won’t be using props here, only the prevState. Here’s how it looks with this form of this.setState():

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

A better way

To round out the example, here’s how I used the second form of this.setState to solve this particular problem.

{
    // ... same as above

    // Version 2
    // Reads the version of this.state that we want.

    handleToggleUseShippingAsBilling() {

        return this.setState(prevState => {

            // If useShippingAsBilling is currently false, set it to `true` 
            // and copy `selectedShippingAddress` to `selectedBillingAddress`

            if (!prevState.useShippingAsBilling) {
                return {
                    useShippingAsBilling: true,
                    selectedBillingAddress: prevState.selectedShippingAddress,
                };
            }

            // If useShippingAsBilling is currently true, set it to `false` 
            // and reset `selectedBillingAddress` to `savedBillingAddress`

            return {
                useShippingAsBilling: false,
                selectedBillingAddress: prevState.savedBillingAddress,
            };
        });
    }

    render() {
        // ... return JSX markup here
    }
}

By using the right form of this.setState(), we can reliably read the prevState value and use that to compute the next state value. Success!

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *