3

Sorry for the confusing title, but here is what is going on:

In MyComponent, I am setting a count state with the useState React hook. Once the component mounts (ie. useEffect with no dependencies), I am instantiaitng two MyClass objects with the first argument as a callback function that increments the state, and the second argument is the timeOut period to call the callback function.

The first instance of MyClass calls the callback in 1000 ms and sets the new value for count, which once updated, is logged in the second useEffect.

However, when the second instance of MyClass calls the call back (after timeOut period of 3000 ms), and tries incrementing the count value, it uses the state of count from when the MyClass was instantiated (which was 0), so it increments count to 1 (wanted behavior is to increment to 2, since the first instance of MyClass already incremented count from 0 to 1)

This is not an issue related to the asynchronicity behavior of setState because it is evident that that first update to count happens before the second instance tries to update it again (the second useEffect gets called when count state is updated, which from the console log messages you can see is happening before second instance of MyClass calls the call back).

JSFiddle link: https://jsfiddle.net/hfv24dpL/

So in conclusion, I think that the issue is that the count state in the callback function is a copy of the count state at the time when the callback functions were passed to the MyClass constructor.

A solution to this example could be to just instantiate the second instance of MyClass when the count state is updated (in the second useEffect), but this is not the solution I am looking for.

Another solution is to use setCount(prevCount => prevCount + 1) to increment count, but this isnt viable in my real application (MyComponent and MyClass are a skeleton example of my real React application that I wrote just for this question).

I want to be able to instantiate the classes togethor when component mounts (in first useEffect), and have the callbacks refer to the most updated version of count.

Is there a solution for this ^ or is there no way around this javascript and React implementation? Thanks for reading all this, I know its long :)

import React, { useState, useEffect } from 'react';

class MyClass{

  constructor(callback, timeOut){

    // call callback in timeOut milliseconds
    this.timeOutId = setTimeout(() => {
      callback();

      }, timeOut)

  }

  clearTimeOut(){

    clearTimeout(this.timeOutId);

  }

}


function MyComponent(){

  var [count, setCount] = useState(0);

  // component did mount
  useEffect(() => {
    let myClass1 = new MyClass(funcToCallback, 1000);
    let myClass2 = new MyClass(funcToCallback, 3000);

    // when component unmounts, clear the timeouts of MyClass instances
    return () => {

      myClass1.clearTimeOut();
      myClass2.clearTimeOut();

    }
  }, []);


  // counter state updated
  useEffect(() => {

    console.log("COUNT UPDATED TO: ", count);

  }, [count])

  // get counter and increment it by 1
  function funcToCallback(){

    console.log("CALLBACK CALLED");
    let newCount = count + 1;
    incCount(newCount);

  }

  function incCount(newCount){

    console.log("NEW COUNT: ", newCount);
    setCount(newCount);

  }

  return (
    <div>
      COUNT: { count }
    </div>
  )

}
Moe Bazzi
  • 85
  • 3

1 Answers1

3

The funcToCallback that gets used is the one in the initial mount of the component, when count is 0. Variables declared with const don't change, and the useEffect callback only gets called once, the count that the funcToCallback closes over remains at 0 forever.

Easiest fix would be to use the function version of the setter instead, which will give you the prior state as an argument - then you can just increment it. Change

  function incCount(newCount){
    console.log("NEW COUNT: ", newCount);
    setCount(newCount);
  }

to

  function incCount(){
    setCount((lastCount) => {
      console.log("NEW COUNT: ", (lastCount + 1));
      return lastCount + 1;
    });
  }
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Hello Jonas, thanks for sharing! Yes I am aware of the closure issue, and I provided the same answer as you did near the end of my post, however I am looking for another solution, because the function version of useState isnt a viable solution in my actual application. (I wrote this code as an example of the closure issue, my application deals with the closure issue but in a different context). – Moe Bazzi Dec 30 '20 at 19:27