在我的 React 自定义挂钩的每次调用中添加和删除事件监听器是否过于昂贵?如何避免?

Is it too expensive to add and remove event listeners on every call of my React custom hook? How to avoid it?

这是我的情况:

我有一个名为 useClick 的自定义挂钩,它获得一个 HTML element 和一个 callback 作为输入,附加一个 click 事件侦听器element,并将callback设置为事件处理程序

App.js

function App() {
  const buttonRef = useRef(null);
  const [myState, setMyState] = useState(0);

  function handleClick() {
    if (myState === 3) {
      console.log("I will only count until 3...");
      return;
    }
    setMyState(prevState => prevState + 1);
  }

  useClick(buttonRef, handleClick);

  return (
    <div>
      <button ref={buttonRef}>Update counter</button>
      {"Counter value is: " + myState}
    </div>
  );
}

useClick.js

import { useEffect } from "react";

function useClick(element, callback) {
  console.log("Inside useClick...");

  useEffect(() => {
    console.log("Inside useClick useEffect...");
    const button = element.current;

    if (button !== null) {
      console.log("Attaching event handler...");
      button.addEventListener("click", callback);
    }
    return () => {
      if (button !== null) {
        console.log("Removing event handler...");
        button.removeEventListener("click", callback);
      }
    };
  }, [element, callback]);
}

export default useClick;

请注意,使用上面的代码,我将在每次调用此挂钩时添加和删除事件侦听器。

而且我非常希望只在挂载时添加和在卸载时删除。而不是在每次调用时添加和删除。

尽管我正在使用这个:

useEffect(()=>{
  // Same code as above
},[element,callback]);  // ONLY RUN THIS WHEN 'element' OR 'callback' CHANGES

问题在于,即使 element (ref) 在所有渲染中保持不变,callback (这是来自 App) 的 handleClick 函数将在每次渲染时改变。所以我最终还是在每个渲染器上添加和删除了处理程序。

我也无法将 handleCLick 转换为 useCallback,因为它取决于每次渲染都会更改的状态 myState 变量,而我的 useCallback 仍然会在每个渲染器上重新创建(当 myState 更改时)并且问题将继续存在。

const handleClick = useCallback(()=> {
  if (myState === 3) {
    console.log("I will only count until 3...");
    return;
  }
  setMyState(prevState => prevState + 1);
},[myState]); // THIS WILL CHANGE ON EVERY RENDER!

问题:

Example on CodeSandbox

我是否应该担心在每次渲染时移除和附加监听器?或者这真的一点也不贵并且不会影响我的表现?有办法避免吗?

您可以将当前回调存储在 ref 中,并为仅调用当前回调的事件侦听器提供静态回调:

function useClick(element, callback) {
    console.log('Inside useClick...');

    const callbackRef = useRef(callback);

    useEffect(() => {
        callbackRef.current = callback;
    }, [callback]);

    const callbackWrapper = useCallback(props => callbackRef.current(props), []);

    useEffect(() => {
        console.log('Inside useClick useEffect...');
        const button = element.current;

        if (button !== null) {
            console.log('Attaching event handler...');
            button.addEventListener('click', callbackWrapper);
        }
        return () => {
            if (button !== null) {
                console.log('Removing event handler...');
                button.removeEventListener('click', callbackWrapper);
            }
        };
    }, [element, callbackWrapper]);
}

工作示例: