React setState usage and gotchas

Nitish Kumar
ITNEXT
Published in
6 min readMar 10, 2018

--

Click here to share this article on LinkedIn

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 with null no longer triggers an update. This means we can decide if the state gets updated within our setState 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 use this.state to get the previous state within setState. Instead, always rely on the above way. Both prevState and props received by the updater function are guaranteed to be up-to-date. The output of the updater is shallowly merged with prevState.

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 within render, 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.

--

--