DevTools.tech

Understanding React setState

November 29, 2019

There are a lot of concepts when it comes to React. State management is one of those which is most widely used and it is a way to hold, process and describe our UI in terms of data. The state is nothing more than a JavaScript object which we can update as per user actions, API responses or any sort of trigger.

With the introduction of React Hooks, we can use state in functional components too. However, in this blog post, we are going to focus on Class-based components and understand how to use setState and their behavior. Moreover, some of the mentioned behaviors hold for React < 17.

class App extends Component {
	constructor(props) {
		super(props);
		this.state = {}; // Initializing state	}
}

Updating state

State can be directly mutated via this.state. However, it will not cause a re-render and will eventually lead to state inconsistency. You can check this behavior using this codesandbox.

Therefore, it is recommended to use setState to perform any state updates. For the rest of the blog post, we are going to use the following example as our base.

class App extends Component {
	state = { items: [], counter: 0 };
	handleClick = () => {
		// some processing
	};
	render() {
		const { items } = this.state;
		console.log('Rendering', items);
		return (
			<div className="App">
				{items.length ? (
					<h2>Items are {JSON.stringify(items)}</h2>
				) : (
					<Fragment>
						<p>No items found</p>
						<button onClick={this.handleClick}>Add items</button>
					</Fragment>
				)}
			</div>
		);
	}
}

Different setState function signatures

setState can be used in two different ways

  1. Passing an object as an argument
...
handleClick = () => {
    const { items } = this.state;
    this.setState({        items: [            ...items,            "apple"        ]    });}
...

In this case, we are passing an object to the setState function which contains a part of the overall state which we want to update. React takes this value and merges it into the final state. Like, in the above code snippet, we are appending an item apple to our items array in the state. After the merging by React, our final state would look like the following

{
	"items": ["apple"],
	"counter": 0
}

React’s state merging is shallow. Hence, it has no impact on counter variable in the state.

  1. Passing function as an argument
...
handleClick = () => {
    this.setState((state, props) => {        const { items } = state;        return {            items: [                ...items,                "apple"            ]        }    });}
...

In this case, we are passing a function to the setState. This function will receive the previous state as the first argument, and the props at that time the update is applied as the second argument. When we talk about the previous state here then it means the most up-to-date state at that point in time (we will see how this is helpful later). It returns an object which would be merged into the final state by React.

Usecases

Now, we are going to observe the behavior of setState invocation in different circumstances and try to understand why it is behaving in that way.

1. Multiple state updates in one scope

In our earlier code snippets, we were appending item(s) to our items array in the state. Suppose, we have a case where we want to make successive setState calls in one function scope. You can use this codesandbox to follow along.

...
handleClick = () => {
    const { items } = this.state;

    this.setState({
        items: [...items, 'apple']
    });
    this.setState({
        items: [...items, 'mango']
    });
    this.setState({
        items: [...items, 'orange']
    });
    this.setState({
        items: [...items, 'pear']
    });
};
...

Open the above-mentioned codesandbox link and check the console.

  1. We will see that initial output would be Rendering [].

Initial Render

  1. Once we click the button then output would be Rendering ["pear"].

After button click

Why does this happen?

Turns out React understands the execution context (important to note) and batches the setState calls as per that. No matter how many successive setState calls we make in a React event handler, it will only produce a single re-render at the end of the event and only last, as the order is preserved, reflects the state.

2. Multiple state updates in one scope using function argument signature

As we have seen above that, only the last setState call is reflected. However, we can rectify this using the function argument signature of setState rather than object based. You can use this codesandbox to follow along.

handleClick = () => {
	this.setState(state => ({
		items: [...state.items, 'apple']
	}));
	this.setState(state => {
		console.log(state);
		return {
			items: [...state.items, 'mango']
		};
	});
	this.setState(state => ({
		items: [...state.items, 'orange']
	}));
	this.setState(state => ({
		items: [...state.items, 'pear']
	}));
};

setState with callback

In this approach, React queues up the callbacks provided to different setState calls and each callback receives the most up-to-date state at that moment as in their execution is synchronous. This is confirmed by the following code snippet

handleClick = () => {
    this.setState(state => ({
        items: [...state.items, 'apple']
    }));
    this.setState(state => {
        console.log(state); // { "items": ["apple"] }        return {
            items: [...state.items, 'mango']
        };
    });
    ...
};

As you can see the state received by the second setState callback already has the item apple in the items array. Since, we are still in the React event handler, all the setState calls will cause a single re-render.

3. Multiple state updates in lifecycle methods

As we have seen above React batches setState calls. However, in current versions (< 17), this is only true inside event handlers and lifecycle methods. You can use this codesandbox to follow along.

...
componentDidMount() {
    const data = ["apple", "orange", "mango", "pear"];
    this.setState(state => {
        return {
            items: { ...state.items, 1: { id: 1, value: data[0] } }
        };
    });
    this.setState(state => {
        return {
            items: { ...state.items, 2: { id: 2, value: data[1] } }
        };
    });
    this.setState(state => {
        return {
            items: { ...state.items, 3: { id: 3, value: data[2] } }
        };
    });
    this.setState(state => {
        return {
            items: { ...state.items, 4: { id: 4, value: data[3] } }
        };
    });
}
render() {
    const { items } = this.state;
    console.log("Rendering...", items);
...

setState in lifecycle methods

There is only one re-render despite multiple state updates. It only happens in React controlled synthetic event handlers.

4. Multiple state updates in AJAX, promises, and setTimeouts

React understands the execution context. No matter where your AJAX, promise or setTimeout is happening as in inside lifecycle methods or event handlers, React will not batch leading to multiple re-renders. You can use this codesandbox to follow along.

...
handleClick = () => {
    this.setState(state => ({
        items: [...state.items, "single"]
    }));
    this.setState(state => ({
        items: [...state.items, "render"]
    }));
    return fakeAPI().then(() => {
        this.setState(state => ({
            items: [...state.items, "apple"]
        }));
        this.setState(state => ({
            items: [...state.items, "mango"]
        }));
        this.setState(state => ({
            items: [...state.items, "orange"]
        }));
        this.setState(state => ({
            items: [...state.items, "pear"]
        }));
    });
};
...

setState outside React controlled methods

As we can see that first two setState calls causes a single re-render. However, calls inside the promise lead to multiple intermediate re-renders.

As per the React team there might be more uniform behavior from React version 17 where batching of setState will happen regardless of the execution context.

Hopefully, this article helped you in some way and if yes, then kindly tweet about it by clicking here Twitter Logo. Feel free to share your feedback here.


Yomesh Gupta

Hi, I am Yomesh Gupta. I am trying to find a perfect blend of design and technology! This is my blog where I write about things which fascinate me. Let me know your views here.

Newsletter.

Subscribe to get notified about new content. No spam ever!