1

I have a component Example in which I want to clear event listener when the local state display changes to false,display is set to true when show prop is true and vice versa.

Code:

const Example = ({ show, onClose }) => {
  const [display, setDisplay] = useState(false);

  const handleClick = (e) => {
    onClose();
    console.log(display);
  };

  useEffect(() => {
    if (display) {
      document.addEventListener('click', handleClick);
    } else {
      document.removeEventListener('click', handleClick);
    }
  }, [display, handleClick]);

  useEffect(() => {
    if (show) {
      setDisplay(show);
    } else {
      setDisplay(false);
    }
  }, [show]);

  return <div>{display && <p>Hi</p>}</div>;
};

Issues:

  • The component is unable to clear the event listener because a new reference of the function handleClick() is made every time Example renders.
  • Every time handleClick() is called, it logs display as true.
  • After 7-8 mouse clicks, the log count in console reaches to 7-8K.

What am I doing wrong here? Thanks :)

Vinay Sharma
  • 3,291
  • 4
  • 30
  • 66
  • looks like you already know the problem. To avoid lose reference to the actual handler, I think you can probably pass the handler via arguments, or use a class component such that the handler will not be created every time the render executes, or create the handler out of the method somewhere... – Surely Apr 26 '20 at 16:56
  • @Surely I am not really using class component, so that can't be solution as such. Other thing, I didn’t understand about passing the handler via arguments. – Vinay Sharma Apr 26 '20 at 17:02

1 Answers1

4

In order to clean up in a useEffect, return a function in the effect. That function will be called every time the effect re-runs and when the component unmounts.

The reason why this will allow for the event listener to be properly removed is because the useEffect creates a closure over the current version of the handleClick function. Which allows the clean-up function to have the same reference so it can be properly cleaned up. It didn't work in the original because every time the useEffect re-ran, a new version of the handleClick was closed over and then the clean-up was tried with the new version.

useEffect(() => {
  if (!display) {
    return;
  }

  document.addEventListener('click', handleClick);

  return () => document.removeEventListener('click', handleClick);
}, [display, handleClick]);

You can further make the effect occur less often by using a ref to the handleClickfunction instead.

For instance at it's most basic. Though you can easily abstract some of the reference and extra use-effect to a separate hook.

const handleClickRef = useRef(handleClick);

useEffect(()=>{
  handleClickRef.current = handleClick;
},[handleClick])

useEffect(() => {
  if (!display) {
    return;
  }
  const funct = (evt)=>handleClickRef.current(evt);
  document.addEventListener('click',funct);

  return () => document.removeEventListener('click', funct);
}, [display]);
Zachary Haber
  • 10,376
  • 1
  • 17
  • 31