Colin Ricardo
Colin Ricardo

Reputation: 17239

Using useEffect with event listeners

The issue I'm having is that when I set up an event listener, the value the event listener sees doesn't update with the state. It's as if it's bound to the initial state.

What is the correct way to do this?

Simple example:

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

const App = () => {
  const [name, setName] = useState("Colin");
  const [nameFromEventHandler, setNameFromEventHandler] = useState("");

  useEffect(() => {
    document.getElementById("name").addEventListener("click", handleClick);
  }, []);

  const handleButton = () => {
    setName("Ricardo");
  };

  const handleClick = () => {
    setNameFromEventHandler(name);
  };

  return (
    <React.Fragment>
      <h1 id="name">name: {name}</h1>
      <h2>name when clicked: {nameFromEventHandler}</h2>
      <button onClick={handleButton}>change name</button>
    </React.Fragment>
  );
};

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

Gif below, since SO code snippet doesn't work for some reason.

enter image description here

Upvotes: 5

Views: 17373

Answers (3)

PJately
PJately

Reputation: 477

I believe the correct solution would be something like this:

  useEffect(() => {
    document.getElementById("name").addEventListener("click", handleClick);
  }, [handleClick]);

  const handleButton = () => {
    setName("Ricardo");
  };

  const handleClick = useCallback(() => {
    setNameFromEventHandler(name)
  }, [name])

The useEffect should have handleClick as part of its dependency array otherwise it will suffer from what is known as a 'stale closure' i.e. having stale state.

To ensure the useEffect is not running on every render, move the handleClick inside a useCallback. This will return a memoized version of the callback that only changes if one of the dependencies has changed which in this case is 'name'.

Upvotes: 2

Pavel Kratochvil
Pavel Kratochvil

Reputation: 481

I think correct solution should be this: codesanbox. We are telling to the effect to take care about its dependency, which is the callback. Whenever it is changed we should do another binding with correct value in closure.

Upvotes: 3

Tom Finney
Tom Finney

Reputation: 2918

So your problem is that you pass an empty array as the second argument to your effect so the effect will never be cleaned up and fired again. This means that handleClick will only ever be closed over the default state. You've essentially written: setNameFromEventHandler("Colin"); for the entire life of this component.

Try removing the second argument all together so the effect will be cleaned up and fired whenever the state changes. When the effect refires, the function that will be handling the click event that will be closed over the most recent version of your state. Also, return a function from your useEffect that will remove your event listener.

E.g.

  useEffect(() => {
    document.getElementById("name").addEventListener("click", handleClick);
    return () => {
      document.getElementById("name").removeEventListener("click", handleClick);
    }
  });

Upvotes: 6

Related Questions