0

There is a React component I'd like to refactor to a functional component using hooks. The function looks like this:

const T = () => {
    const [idx, setIdx] = React.useState<number>(0);
    
    ...

    const func1 = (e: React.MouseEvent<HTMLDivElement>, c: number) => {
        setIdx(c);
        window.addEventListener("mousemove", func2);
    }
    
    const func2 = (e: MouseEvent) => {
        console.log(idx);
    }
}

func1 is something called on mouse down. func2 is something that gets called by an event listener while the mouse is dragged. The whole function is quite complex so I'm hoping for minimal restructuring.

How can I get func2 to get the new version of idx?

*Edit: For some reasons, this has been marked as a duplicate of The useState set method is not reflecting a change immediately The accepted solution from the link (useEffect) does not work in my example for removing event listeners. In my post, the first solution of the accepted answer works (the second part doesn't).

matohak
  • 535
  • 4
  • 19
  • Why are you adding the `mousemove` event listener in the `func1`, shouldn't it be in a `useEffect`. I upvoted @Ismail_Aj's answer, it seems the most reasonable here given that state updates are not instant. – Abdulrahman Ali Aug 19 '22 at 11:55

2 Answers2

1

If you want idx to be updated immediately don't you should look for a solution combining useEffect and useRef hooks, due to the fact that useState works asynchronously it waits until it gathers all state updates then perform them all for performance.

const refIdx = useRef<number>(0);
   const func1 = (e: React.MouseEvent<HTMLDivElement>, c: number) => {
        refIdx.current = c;
        window.addEventListener("mousemove", func2);
    }
useEffect(() => {
 // perform the action you want to trigger on idxChange here
}, [refIdx.current]}
Ismail_Aj
  • 332
  • 1
  • 4
  • 17
1

The issue is that func2 is defined when the component is loaded/updated, and since state updates are asynchronous, then when you're adding the event listener, it sets the currently defined func2 with the original idx as the callback.

What you can do is to wrap func2 in a function that takes idx as a parameter and returns a function that can then act as the callback.

Then you add the listener in setState's callback to use the new state value right away.

const { useState } = React

const T = () => {
    const [idx, setIdx] = useState(0);
    
    const func1 = (e, c) => {
        setIdx(prev => {
          let newIdx = 1; // 1 instead of c for the example
          window.addEventListener("mousemove", func2(newIdx)); // call outer func2 with new state
          return newIdx // set new state
        });
    }
    
    const func2 = idx => (e) => {
        console.log(idx);
    }
    
    return <div id="drag-me" onClick={func1}>Click Here</div>
}

ReactDOM.render(
<T />,
window.root
)
#drag-me{
width: 100px;
height: 100px;
background-color: beige;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

<div id="root"></div>

You can also use useEffect to add the event listener, but it'll run each time idx is set, including when the component first mounts. You can use states to control this. For example, I used a state that represents func1 being called.

const { useState, useEffect } = React

const T = () => {
    const [idx, setIdx] = useState(0);
    const [funcCalled, setFuncCalled] = useState(false);
    
    useEffect(()=> {
      if(!funcCalled) return
      const func2 = e => console.log(idx);
      window.addEventListener('mousemove', func2);
      return () =>{
        if (!funcCalled) return
        window.removeEventListener('mousemove', func2)
      } // Cleanup after component unmounts
    },[funcCalled]) 
    
    const func1 = (e, c) => {
        setIdx(1) // 1 instead of c for the example
        setFuncCalled(true) // React respects state update order
    }
    
    
    return <div id="drag-me" onClick={func1}>Click Here</div>
}

ReactDOM.render(
<T />,
window.root
)
#drag-me{
width: 100px;
height: 100px;
background-color: beige;
}
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>

<div id="root"></div>

Don't worry about the order of state updates, react will update idx first becfore updating funcCalled. Source

Brother58697
  • 2,290
  • 2
  • 4
  • 12