React setState usage and gotchas
A React class component has an internal state which like props affects how the component renders and behaves. Unlike props, state is local to the component and can only be initialised and updated within the component.
Initialisation
Before we can use state, we need to declare a default set of values for the initial state. This can be done by either creating a state object in the constructor or directly within the class.
class Counter extends React.Component {
constructor(props, context) {
super(props, context)
this.state = {
quantity: 1,
counter: 0
}
}
}class Counter extends React.Component {
state = {
quantity: 1,
counter: 0
}
}
Update
State can be updated in response to event handlers, server responses or prop changes. React provides a method called setState
for this purpose.
setState()
enqueues changes to the component state and tells React that this component and its children need to be re-rendered with the updated state.
this.setState({quantity: 2})
Here, we passed setState()
an object containing part(s) of the state we wanted to update. The object passed would have keys corresponding to the keys in the component state, then setState()
updates or sets the state by merging the object to the state.
setState and re-rendering
setState()
will always lead to a re-render unless shouldComponentUpdate()
returns false
. To avoid unnecessary renders, calling setState()
only when the new state differs from the previous state makes sense and can avoid calling setState()
in an infinite loop within certain lifecycle methods like componentDidUpdate
.
React 16 onwards, calling
setState
withnull
no longer triggers an update. This means we can decide if the state gets updated within oursetState
method itself!
Signature
setState(updater[, callback])
The first argument is an updater
function with the signature:
(prevState, props) => stateChange
prevState
is a reference to the previous state. It should not be directly mutated. Instead, changes should be represented by building a new object based on the input from prevState
and props
. For example, to increment a value in state by props.step
:
this.setState((prevState, props) => {
return {counter: prevState.counter + props.step};
})
Due to the async nature of
setState
, it is not advisable to usethis.state
to get the previous state withinsetState
. Instead, always rely on the above way. BothprevState
andprops
received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged withprevState
.
The second parameter to setState()
is an optional callback function that will be executed once setState
is completed and the component is re-rendered. componentDidUpdate
should be used instead to apply such logic in most cases.
You may directly pass an object as the first argument to setState
instead of a function. This performs a shallow merge of the state change into the new state.
this.setState({quantity: 2})
Batching state updates
In case multiple setState()
calls are made, React may batch the state updates while respecting the order of updates. Currently (React 16 and earlier), only updates inside React event handlers are batched by default. Changes are always flushed together at the end of the event and you don’t see the intermediate state.
It doesn’t matter how many setState()
calls in how many components you do inside a React event handler, they will produce only a single re-render at the end of the event. For example, if child and parent each call setState()
when handling a click event, the child would only re-render once.
Till React 16, there is no batching by default outside of React event handlers. So, each setState()
would be processed immediately as it happens if they lie outside any event handler. For example:
promise.then(() => {
// We're not in an event handler, so these are flushed separately.
this.setState({a: true}); // Re-renders with {a: true, b: false }
this.setState({b: true}); // Re-renders with {a: true, b: true }
})
However, ReactDOM
provides an api which could be used to force batch updates.
promise.then(() => {
// Forces batching
ReactDOM.unstable_batchedUpdates(() => {
this.setState({a: true}); // Doesn't re-render yet
this.setState({b: true}); // Doesn't re-render yet
});
// When we exit unstable_batchedUpdates, re-renders once
})
Internally React event handlers are all wrapped in unstable_batchedUpdates
due to which they're batched by default. Wrapping an update in unstable_batchedUpdates
twice has no effect. The updates are flushed when the outermost unstable_batchedUpdates
call exits.
The API is “unstable” in the sense that it will be removed when batching is already enabled by default in the React core.
Declaring state changes outside of React Component
This section is inspired by the below tweet from Dan Abramov.
As setState
expects a function in the argument, the function can be implemented somewhere outside of the React Class and then imported and used inside the Component as the argument to setState
. In case extra arguments are needed, higher order functions can come to the rescue.
const multiplyBy = multiplier => state => ({
value: state.value * multiplier
})
As state updates are now plain JavaScript, testing complex state transitions won’t involve shallow rendering of React components.
setState and Lifecycle methods
Calling setState
in lifecycle methods requires a level of caution. There are a few methods where it doesn’t make sense to call setState and there are a few where it should be called conditionally. Let’s take it case by case.
componentWillMount
setState
can be called here. The updated state would be used inside the immediate render as long as it is not calculated based on Promise resolution. It is advised to used componentDidMount
for performing any state updates on Promise resolution, etc. The other reason to not use this method is that in the future releases of React(17 onwards), this method is going to be deprecated.
componentDidMount
This is the preferred method for async rendering.
componentWillReceiveProps
The main purpose of this method is to calculate some values derived from props for use during render. Although, if the calculation is fast enough it could just be done in render
. setState
is perfectly safe to be used here.
shouldComponentUpdate
This is one of those methods where it doesn’t make sense to update component’s state. It is invoked internally by React during the update phase (props or state change). Calling setState
here would result in an infinite loop as it is the next method that it called on updating state. If you need to set state in the props update phase, use componentWillReceiveProps
.
componentWillUpdate
Don’t use setState
here. Similar reasons as shouldComponentUpdate
.
render
Calling setState
here makes your component a contender for producing infinite loops. render
should remain pure and be used to conditionally switch between JSX fragments/child components based on state or props. Callbacks in render can be used to update state and then re-render based on the change.
If you find yourself having to write
setState
withinrender
, you may want to rethink the design. In fact, this may be a perfect use-case to implement the state machines pattern.
componentDidUpdate
The most common use case for calling setState
here is to update the DOM in response to prop or state changes. Here, you wait for the component to get rendered before updating the state again. This makes it a candidate for setting state values which is dependent on the rendered DOM values. Remember to check if you are updating the same state again as this method would be called again after setState()
call . For example:
componentDidUpdate = (prevProps, prevState) => {
let width = ReactDOM.findDOMNode(this).parentNode.offsetWidth
if (prevState && prevState.width !== width) {
this.setState({ width })
}
}
componentWillUnmount
Don’t use setState
here. Your component is going away.
References
- https://reactjs.org/docs/react-component.html#setstate
- https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973
P.S. If the article helped you, make sure to clap👏 , follow me on twitter, and share with your friends!