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:
- 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 calleduseShippingAsBilling
. To use this form ofthis.setState()
, we would read the value first, and then usethis.setState
to write the new value.const { useShippingAsBilling: prevValue, } = this.state; this.setState({ useShippingAsBilling: !prevValue; });
- 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:- The previous state value, or
prevState
- The current
props
- The previous state value, or
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!