'State isn't updated in a function if that function is called outside of the hook

I have an issue and i don't know how to solve it.

Basically, the idea is that i have a class that i need to instanciate only once (instanciation is done in an useMemo in order to have it done only once), this class is in reality a websocket implementation which calls a handler (here onCounterChanged) when it receives a message from the backend), in order to update a component state, but for some reason this handler holds the old counter state value. Here is the code :

class MyService {
  constructor(onCounterChange) {
    this.onCounterChange = onCounterChange;
  }
  start = () => {
//This setInterval is only used to try to reproduce the reality (an async event so for example 
//when a message is received from the server). So i call this onCounterChange every 2 sec to 
//simulate a message received from the server
    setInterval(() => {
      this.onCounterChange();
    }, 2000);
  };
}

const MyComponent = () => {
  const [counter, setCounter] = React.useState(0);
  const onCounterChanged = () => {
    console.log("onCounterChanged", counter);
    setCounter(counter + 1);
  };
//i'm using useMemo in order to keep the instance of MyService, because i'm doing some stuff
//(connect to the webSocket etc... in this class, i don't want to create an new instance on every 
//render and do the connection to the server again
  const myService = React.useMemo(() => new MyService(onCounterChanged), []);
  React.useEffect(() => {
    myService.start();
  }, []);
  return <div>{counter}</div>;
};

Here is a sandbox to see what's happening : https://codesandbox.io/s/blazing-tree-z1m99e?file=/src/App.js

You can see that the counter isn't incremented, the function onCounterChange is called every seconds but the counter state is not updated inside of that handler. I tried to fix it by using useCallback but nothing was working.

Any idea how can i fix this ? I'm not a pro of react so, sorry if i'm missing something.

Thanks for reading.



Solution 1:[1]

There is an issue with your update state, I did a little refactor.

When you try to update the state using the old value, you should use a callback instead of passing the "current" value, avoid passing the current state value because is not always in sync with the actual state value

const onCounterChanged = useCallback(() => {
    setCounter((currentValue) => {
      console.log({ currentValue });
      return currentValue + 1;
    });
  }, [setCounter]);

Using a callback will assure you that is using the latest value

Here is a CodeSandbox: https://codesandbox.io/s/goofy-sanderson-bfq3mh?file=/src/App.js

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Link Strifer