John
John

Reputation: 159

Why does state not update correctly when calling setState() multiple times

When clicking the button, the text "second message" and "third message" should be added to the state through these functions:

class Button extends React.Component {

    constructor(props) {
        super(props);
        this.addMessage = this.addMessage.bind(this);
    }

    addMessage() {
        this.props.addMessage('second message'); // This is ignored
        this.props.addMessage('third message');  // Only this message is added
    }

    render() {
        return(
            <button onClick={this.addMessage}>Add text</button>
        )
    }
}

But only one of them is firing.

I created a simple snippet that shows the problem.

class Button extends React.Component {

	constructor(props) {
		super(props);
		this.addMessage = this.addMessage.bind(this);
	}
	
	addMessage() {
		this.props.addMessage('second message'); // This is ignored
		this.props.addMessage('third message');	 // Only this message is added
	}
	
	render() {
		return(
			<button onClick={this.addMessage}>Add text</button>
		)
	}

}

class Messages extends React.Component {

	constructor(props) {
		super(props);
	}
	
	render() {
		return(
			<p>{this.props.message}</p>
		)
	}

}

class App extends React.Component {
	
	constructor(props) {
		super(props);
		this.state = {
			messages: [{
				text: 'first message'
			}]
		}
		this.addMessage = this.addMessage.bind(this);
	}
	
	addMessage(message) {
		let messages = this.state.messages.slice();
		messages.push({text:message});
		this.setState({messages: messages});
	}
	
	render() {
		let messages = [];
		this.state.messages.forEach((message) => {
			messages.push(<Messages message={message.text}></Messages>);
		});
		return(
			<div>
				{messages}
				<Button addMessage={this.addMessage} />
			</div>
		)
	}
}

ReactDOM.render(
	<App />,
	document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.0.0/react-dom.min.js"></script>

<div id="root"></div>

Thanks in advance!

Upvotes: 3

Views: 3344

Answers (1)

trixn
trixn

Reputation: 16354

State Updates May Be Asynchronous

Quoted from the reactjs docs about state updates:

React may batch multiple setState() calls into a single update for performance.

Because this.props and this.state may be updated asynchronously, you should not rely on their values for calculating the next state.

Your second call to addMessage() uses this.state to set the new state. But because setState() is asynchronous the state my not have been updated yet which leads to your second call overriding your first one:

addMessage(message) {

    // here you access this.state which hasn't been updated yet in your second call
    let messages = this.state.messages.slice();

    messages.push({text: message});
    this.setState({messages: messages});
}

To fix it, use a second form of setState() that accepts a function rather than an object. That function will receive the previous state as the first argument, and the props at the time the update is applied as the second argument:

Use an arrow function that gets passed the previous state including changes of preceding calls to setState()to calculate the new state. That will fix your issue.

Using es6 spread syntax:

this.setState((prevState, props) => ({
    messages: [...prevState.messages, {text: message}]
}));

Using slice():

this.setState((prevState, props) => {
    let messages = prevState.messages.slice();
    messages.push({text:message});
    return {messages};
});

Upvotes: 10

Related Questions