Home > Back-end >  Why is calling setState twice causing a single state update?
Why is calling setState twice causing a single state update?

Time:12-29

This is probably a beginner React mistake but I want to call "addMessage" twice using "add2Messages", however it only registers once. I'm guessing this has something to do with how hooks work in React, how can I make this work?

export default function MyFunction() {
  const [messages, setMessages] = React.useState([]);

  const addMessage = (message) => {
    setMessages(messages.concat(message));
  };

  const add2Messages = () => {
    addMessage("Message1");
    addMessage("Message2");
  };

  return (
    <div>
      {messages.map((message, index) => (
        <div key={index}>{message}</div>
      ))}
      <button onClick={() => add2Messages()}>Add 2 messages</button>
    </div>
  );
}

I'm using React 17.0.2

CodePudding user response:

When a normal form of state update is used, React will batch the multiple setState calls into a single update and trigger one render to improve the performance.

Using a functional state update will solve this:

const addMessage = (message) => {
  setMessages(prevMessages => [...prevMessages, message]);
};

const add2Messages = () => {
  addMessage('Message1');
  addMessage('Message2');
};

More about functional state update:

Functional state update is an alternative way to update the state. This works by passing a callback function that returns the updated state to setState.

React will call this callback function with the previous state.

A functional state update when you just want to increment the previous state by 1 looks like this:

setState((previousState) => previousState   1)

The advantages are:

  1. You get access to the previous state as a parameter. So when the new state depends on the previous state, the parameter is helpful as it solves the problem of stale state (something that you can encounter when you use normal state update to determine the next state as the state is updated asynchronously)

  2. State updates will not get skipped.

  3. Better memoization of handlers when using useCallback as the dependencies can be empty most of the time:

    const addMessage = useCallback((message) => {
      setMessages(prevMessages => [...prevMessages, message]);
    }, []);
    

CodePudding user response:

import React from "react";

export default function MyFunction() {
  const [messages, setMessages] = React.useState([]);

  const addMessage = (message) => {
    setMessages(messages => [...messages, message]);
  };

  const add2Messages = () => {
    addMessage("Message1");
    addMessage("Message2");
  };

  return (
    <div>
      {messages.map((message, index) => (
        <div key={index}>{message}</div>
      ))}
      <button onClick={() => add2Messages()}>Add 2 messages</button>
    </div>
  );
}

CodePudding user response:

This is because messages still refers to the original array. It will get the new array at the next re-render, which will occur after the execution of add2Messages.

Here are 2 solutions to solve your problem :

  1. Use a function when calling setMessages
export default function MyFunction() {
  const [messages, setMessages] = React.useState([]);

  const addMessage = (message) => {
    setMessages(prevMessages => prevMessages.concat(message));
  };

  const add2Messages = () => {
    addMessage("Message1");
    addMessage("Message2");
  };

  return (
    <div>
      {messages.map((message, index) => (
        <div key={index}>{message}</div>
      ))}
      <button onClick={() => add2Messages()}>Add 2 messages</button>
    </div>
  );
}
  1. Modify addMessage to handle multiple messages
export default function MyFunction() {
  const [messages, setMessages] = React.useState([]);

  const addMessage = (...messagesToAdd) => {
    setMessages(prevMessages => prevMessages.concat(messagesToAdd));
    // setMessages(messages.concat(messagesToAdd)); should also work
  };

  return (
    <div>
      {messages.map((message, index) => (
        <div key={index}>{message}</div>
      ))}
      <button onClick={() => addMessage("Message1", "Message2")}>
        Add 2 messages
      </button>
    </div>
  );
}

CodePudding user response:

Changing addMessage function as below will make your code work as expected

  const addMessage = (message) => {
    setMessages(messages => messages.concat(message));
  };

Your code didn't work because in case of synchronous event handlers(add2Messages) react will do only one batch update of state instead of updating state after every setState calls. Which is why when second addMessage was called here, the messages state variable will have [] only.

const addMessage = (message) => {
  setMessages(messages.concat(message));
};
const add2Messages = () => {
  addMessage('Message1'); // -> [].concat("Message1") = Message1
  addMessage('Message2'); // -> [].concat("Message2") = Message2
};

So if you want to alter the state value based on previous state value(especially before re-rendering), you can make use of functional updates.

  • Related