简体   繁体   中英

missing dependency warning when working with event listeners and states inside useEffect

Everytime I work with addEventListener() , and also want to access some state inside useEffect , I get the same issue. I can't add the state as dependency, because then I would create multiple event listeners each time the state changes. I almost everytime find myself stuck with the " React Hook useEffect has a missing dependency " warning. Let's say I have a component that needs to change it state on window.onClick() and on window.onDoubleClick() . If the state is true, click should change it to false, and if the state is false, double click should change it to true. So here's what I whould write:

import React, { useState, useEffect } from 'react';

export default function someComponent() {
  const [toggle, setToggle] = useState(false);

  useEffect(() => {
    window.addEventListener('click', (event) => {
      if (toggle) setToggle(false)
    })

    window.addEventListener('dblclick', (event) => {
      if (!toggle) setToggle(true)
    })
  }, [])

  return (
    <p>The toggle state is {toggle.toString()}</p>
  );
}

This code works, but I get the missing dependency warning. I can't add toggle to the dependency array, because then it will add another event listener each time the toggle state changes.

What am I doing wrong here? how should I fix this?

Edit: Maybe this example wasn't too good, but it's the simplest I could think of. But, this issue is also for when I create other event listeners, that have to be on the windows object, like scroll. I know I can use return to remove the event listener everytime, but for events like scroll it makes it much slower. It doesn't make sense to me that I have to remove and add it everytime, when I just don't need it to fire again.

With react you don't have to use the window element in this case. Not even a useEffect.

By using the useEffect hook you are telling react to do something after render (depending on the dependency array). In this case changing state is not necessary immediately after rendering the page, only when the user interacts with the element.

Adding click events through the useEffect is probably not needed most of the time and and doing it like the example below will probably save you time and a headache and maybe even performance (correct me if i'm wrong).

I would personally do it like this.

import React, { useState } from 'react';
        
export default function someComponent() {
  const [toggle, setToggle] = useState(false);

  return (
    <p 
      onClick={() => setToggle(false)} 
      onDoubleClick={() => setToggle(true)}
    >
      The toggle state is {toggle.toString()}
    </p>
  );
}

You could also call functions from the element like so

  const [toggle, setToggle] = useState(false);

  const handleClick = () => {
    if (toggle) {
      setToggle(false);
    }
  };

  const handleDoubleClick = () => {
    if (!toggle) {
      setToggle(true);
    }
  };

  return (
    <p 
      onClick={() => handleClick()} 
      onDoubleClick={() => handleDoubleClick()}
    >
      The toggle state is {toggle.toString()}
    </p>
  );

CodeSandbox example

编辑 staging-water-cuwo4

You can add a clean-up function to the useEffect hook to remove old listeners. This way you can pass toggle into the dependency array and you won't have stacking event listeners.

https://reactjs.org/docs/hooks-effect.html

 useEffect(() => { const handleClick = () => toggle? setToggle(false): setToggle(true); window.addEventListener('click', handleClick); window.addEventListener('dblclick', handleClick); return () => { window.removeEventListener('click', handleClick); window.removeEventListener('dblclick', handleClick); } }, [toggle]);

I can't add the state as dependency, because then I would create multiple event listeners each time the state changes.

There is a way around this, and that is toreturn a cleanup function from the useEffect callback. I would encourage you to read the linked section of the docs, then the below solution would become much clearer:

useEffect(() => {
  const handleClick = () => {
    setToggle(!toggle)
  }

  window.addEventListener('click', handleClick)    

  return () => {
    window.removeEventListener('click', handleClick)
  }
}, [toggle])

with the above solution, each time toggle is updated, the cleanup function is called, which removes the current event listener before running the effect again.

Also note that you can provide a callback function to setToggle , which receives the current value of toggle and returns the new value. With this approach you wouldn't need to pass toggle as a dependency to useEffect:

useEffect(() => {
  const handleClick = () => {
    setToggle(currentValue => !currentValue)
  }
  
  window.addEventListener("click", handleClick)

  return () => {
    window.removeEventListener("click", handleClick)
  }
}, [])

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM