Is there a way to call useEffect only once and also have access to the current state of component?

Viewed 3353

I'm using socket.io along with react.js. I need to declare some event handlers for the socket, Also because of some modules I'm using, I can't use classic class components and therefore I'm using functions along with hooks. The problem is initializing the event handlers using the useEffect hook results in redeclaring the socket event handlers every time a new render happens, Thus, each event gets connected with a bunch of event handlers instead of only one, which results in multiple calls of the desired event handler when the event happens. There is way to make the useEffect run only once; Passing an empty array as a second argument to it (which kind of makes it like a constructor in class components). This prevents multiple calls on the event handler but, This also prevents the event handler from accessing the components latest state. Is there a way to achieve both?

var socket = io('http://localhost:10000');

const [var, setvar] = useState(0);
useEffect(() => {
   socket.on('event', function(data) { //event handler function
     console.log(var);
   });
}, []);

For further explanation, The code above calls the event handler function only once per event but it always outputs the initial state of var which is 0. On the other hand, if the [] argument is removed, The handler will output the current value of var but it outputs it multiple times, Because for each render a new event handler is connected to the socket so the number of outputs depends on the number of renders that happened before that event.

2 Answers

Instead of the square brackets, you could pass in a 'dependency' for example [isInit]. The effect will only run if the value of isInit changes. You should declare isInit using useState and default it to false, i.e const [isInit, setIsInit] = useState(false). Then when you run the useEffect the first time call set setIsInit(true). Then in the useEffect check if (!isInit) ... this will control it nicely for you.

You can try combining what you have with a useRef, something like:

var socket = io('http://localhost:10000');

const [var, setvar] = useState(0);
const ref = useRef({current: var});

const alteredSetvar = useCallback((var) => {
  setvar(var);
  ref.current = var;
}, [setvar, ref]);

useEffect(() => {
   socket.on('event', function(data) { //event handler function
     console.log(ref.current);
   });
}, []);

would work I think (call alteredSetvar instead of setvar.

useRef is typically used to hold a reference to a DOM node, but in reality it just holds an immutable reference, of which you can alter the inner value as much as you needed.

Related