Search code examples
reactjsstate

I am updating state without mutating it, still I am not getting old state value in next renders of component


I am very new to react and trying to learn basic things. While writing some test programs, I ran into a problem: Basically I wanted to add new object to array after every 2 seconds and I am doing it with the help of useEffect and setInterval. I followed the rule of react to not to mutate the state variable, so I created a new variable from old state and set that variable to state setter function. Still I found, it was not giving me updated state.

const [liveChats, setLiveChats] = useState([]);

  useEffect(() => {
    const interval = setInterval(() => {
      const newLiveChats = [...liveChats];
      newLiveChats.push({
        name: generateRandomName(),
        message: generateRandomMessage(20),
      });
      setLiveChats(newLiveChats);
    }, 2000);

    return () => clearInterval(interval);
  }, []);

I managed to get the updated state with state updater function (as shown below), but I would like to understand what was wrong with old code. What's the rule of thumb here.

const [liveChats, setLiveChats] = useState([]);

  useEffect(() => {
    const interval = setInterval(() => {
      setLiveChats((prevState) => [
        {
          name: generateRandomName(),
          message: generateRandomMessage(20),
        },
        ...prevState,
      ]);
    }, 2000);

    return () => clearInterval(interval);
  }, []);

Solution

  • Your useEffect dependency array determines when it will re-run.

    In your first snippet, your dependency array is empty, which means liveChats will only ever been the initial value of liveChats, which is an empty array.

    So even if you push a new object to newLiveChats and update liveChats, on the next interval liveChats will not persist the newly pushed object.

    Your second snippet works successfully because the functional update of useState lets you pass a function that receives the previous state as an argument, which ensures you're always adding onto the latest version of liveChats.

    For illustration, you could modify your first code snippet like so:

    const [liveChats, setLiveChats] = useState([]);
    
    useEffect(() => {
      const interval = setInterval(() => {
        setLiveChats([
            ...liveChats,
            {
                name: generateRandomName(),
                message: generateRandomMessage(),
            }
        ]);
      }, 2000);
    
      return () => clearInterval(interval);
    }, [liveChats]); // Now it will re-run every time liveChats updates
    

    In this example, I've added liveChats to the useEffect dependency array, so it will re-trigger it every time liveChats updates.

    As a result, liveChats in the interval function will be the latest value.

    Without the interval, however, this would be a bad practice because it would cause an infinite loop.