0

I have a React component like so:

https://codepen.io/darajava/pen/NWrdWeP?editors=0010

function TestUseState() {
  const [count, setCount] = useState(0);
  const buttonRef = useRef(null);

  useEffect(() => {
    buttonRef.current.addEventListener("click", () => {
      alert(count);
    });

    setCount(10000);
  }, []);
  
  return (
    <div ref={buttonRef}>
      Click
    </div>
  )
}

I set up an event listener on the button and it seems to take the initial value of the state, after setting it directly afterwards. Is this expected behaviour? How can I avoid this?

The actual code in question (not valid, only relevant parts added):

const Game = () => {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const [player, setPlayer] = useState<IPlayer>();

  const setPosition = (newPos) => {
    console.log(player); // <--- player is undefined on mousemove
  };

  useEffect(() => {
    canvas = canvasRef.current;
    ctx = canvas.getContext('2d');
    
    canvas.addEventListener('mousemove', (e: MouseEvent) => {
      setPosition(getCursorPosition(canvas, e));
    });
  }, []);


  return (
    <canvas ref={canvasRef} styleName="canvas" />
  );
}
Dara Java
  • 2,410
  • 3
  • 27
  • 52
  • You've closed over initial `count` state into the click callback. Just use the normal `onClick` handler you can attach to an element. – Drew Reese Oct 23 '20 at 06:25
  • Thank you Drew, but this is a very simplified version of what I am actually trying to do. I am using "mousemove" on the canvas API. I thought the fat arrow function was supposed to prevent a closure? – Dara Java Oct 23 '20 at 06:27
  • This is a "stale closure": https://stackoverflow.com/questions/53633698/referencing-outdated-state-in-react-useeffect-hook – Linda Paiste Oct 23 '20 at 06:27
  • If you add `[count]` to the dependency array then it sets the count to 10000 and immediately rerenders. – Linda Paiste Oct 23 '20 at 06:30
  • If I add `[count]` to the dependency array the click listener will be added each time that `count` changes. – Dara Java Oct 23 '20 at 06:31
  • That is what effect hook cleanup functions are for. But would be extremely wasteful for this trivial example. Can you update question to include an example closer to your actual use case? Better to find solution for your real issue than for this contrived example. In React it is preferred to attach click handlers to the react components as opposed to adding event listeners, when you manually add listeners you have to manually maintain them. – Drew Reese Oct 23 '20 at 06:32
  • @DrewReese, I have added the real code. – Dara Java Oct 23 '20 at 06:38

1 Answers1

1

Yes this is expected, but I can understand why it seems strange.

When you are adding the ref in the useEffect hook you are closing over the value of count and saving it for later, so when you click the sub it shows you the value when the component was initialized.

If you want to alert the actual value of count you can add onClick={()=>alert(count)} to the div, this is also more in following the declarative style of React.

You are discouraged to use refs in React because React maintains a virtual dom. You use refs when you need to access dom elements directly.

Edit: You can also use this for the mouse move event:

https://reactjs.org/docs/events.html#mouse-events

Write the handler function separately in the body of the functional component and pass it to canvas element's onMouseMove prop.

Sebastiaan
  • 980
  • 9
  • 10
  • 1
    Hey nice! I didn't realise I could just use `onMouseMove` directly on the element. Works for me. Thanks. – Dara Java Oct 23 '20 at 06:41