Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Why is useState within useEffect not working in React?

I use useEffect() to get a Firestore snapshot and parallel I want to count a value:

  const [counter, setCounter] = useState({ mona: 0, phil: 0 });
  useEffect(() => {
    onSnapshot(q, (snapshop) => {
      setTasks(
        snapshop.docs.map((doc) => {
          if (doc.data().wer === "Mona") {
            console.log("Mona + 1"); // This get's executed as expected (e.g. 3 times)
            setCounter({ ...counter, mona: counter.mona + 1 });
          }
          if (doc.data().wer === "Phil") {
            console.log("Phil + 1"); // This get's executed as expected (e.g. 6 times)
            setCounter({ ...counter, phil: counter.phil + 1 });
          }
          return {
            ...doc.data(),
            id: doc.id,
            timestamp: doc.data().timestamp?.toDate().getTime(),
          };
        })
      );
      setLoading(false);
    });
  }, []);

  useEffect(() => {
    console.log({ counter }); //this get's executed only 2 times.
  }, [counter]);

When the console.log() within the map() get executed correct, why does the setCounter doesn’t execute or update the counter correct?

The console.log({ counter }); btw gives nothing more than:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

{counter: {mona: 0, phil: 0}}
{counter: {mona: 0, phil: 1}}

>Solution :

React sometimes batches updates to the state. Which means all your call to setCounter only trigger one effect.

Moreover the value of counter inside your function is also updated at the end of the function, therefore you are losing updates.

What you should do:

  • First of all pass a callback to setCounter instead of using the value of counter. So change:

    setCounter({ mona: counter.mona, phil: counter.phil + 1 });
    

    to:

    setCounter(counter => ({ mona: counter.mona, phil: counter.phil + 1 }));
    
  • To force useEffect to be called multiple times you have to opt-out of batched updates using ReactDOM.flushSync:

    import { flushSync } from 'react-dom';
    
    // ...
    
    flushSync(() => setCounter(counter => ({ mona: counter.mona, phil: counter.phil + 1 })));
    

    In this way your useEffect should be called for every single change of the counter. Obviously this is less efficient than having the updates batched.


Since you are reloading the whole dataset everytime you want to re-count everything on each call to onSnapshot instead of simply modifying the current value.

In that case you can do this:

const newCounter = { mona: 0, phil: 0};
snapshop.docs.map((doc) => {
          if (doc.data().wer === "Mona") {
            console.log("Mona + 1"); // This get's executed as expected (e.g. 3 times)
            newCounter.mona += 1;
          }
          if (doc.data().wer === "Phil") {
            console.log("Phil + 1"); // This get's executed as expected (e.g. 6 times)
            newCounter.phil += 1;
          }
          // ...
});
setCounter(newCounter);

So you just compute the result and call setCounter once outside the loop with the final count. In this case you don’t need to read the old state since you recompute it from scratch.

You could keep the old code and add a setCounter({mona: 0, phil: 0}) outside the loop, but I believe it would be less efficient than computing the values outside react hooks and only calling the setCounter once.

Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading