React/Redux、useEffect/useSelector、过时的闭包和事件侦听器

React/Redux, useEffect/useSelector, stale closures and event listeners

我有一个 React/Redux class 组件,我正在将其转换为功能组件。

以前,它有一个 componentDidMount 回调,它为从应用中其他地方调度的自定义事件添加了一个事件侦听器。这将被 useEffect 取代。事件侦听器在触发时调用此组件中其他位置的方法。

此方法执行的操作取决于从选择器中检索到的值。最初我传递 useEffect 一个空的依赖项数组,所以它只会在挂载时添加事件侦听器。然而,显然这会导致围绕选择器值的陈旧闭包。一个可行的解决方案是将选择器作为依赖项传递给它——但是,这会导致每次选择器的值发生变化时监听器都是 removed/re-added,这不是很好。

我正在考虑一种解决方案,它只添加一次事件侦听器,同时允许被调用的方法访问选择器的当前值。

示例代码:

const currentValue = useSelector(state => getValue(state));

useEffect(() => {
  document.addEventListener('my.custom.event', handleEvent);

  return(() => document.removeEventListener('my.custom.event', handleEvent));
}, []);

const handleEvent = () => {
  console.log(currentValue)
}

照原样,这会在 currentValue 周围创建一个陈旧的闭包,因此在事件触发时,记录的值不一定是最新的。

useEffect 中将 [] 更改为 [currentValue] 会产生预期的行为,但 removes/re-adds 每次更改 currentValue 时都会监听事件。

由于这不是组件的状态值,因此无法使用 console.log(currentValue => console.log(currentValue)) 之类的回调来访问最新值。我也尝试过使用 useRef 来保留值,但我相信每次选择器的值发生变化时我都需要一些方法来更新 ref 值,这不是一个很好的解决方案。

在实际组件中,currentValue的值在Redux中被其他组件修改,因此将其更改为状态值也不太可行。

我在想:

useRef 方法通常是解决此问题的方法,但您需要另一个 useEffect 来使用 currentValue:

更新 ref
const currentValue = useSelector(state => getValue(state));
const valueRef = useRef();

useEffect(() => { valueRef.current = currentValue; }, [currentValue]);

useEffect(() => {
  const handleEvent = () => {
    console.log(valueRef.current)
  }

  document.addEventListener('my.custom.event', handleEvent);

  return(() => document.removeEventListener('my.custom.event', handleEvent));
}, []);

但是,您也可以从 useEffect 中提取整个函数,并在挂钩中使用它,这样您就可以轻松地为事件处理创建自定义挂钩:

const useEventHandler = (eventName, eventHandler, eventTarget = document) => {
  const eventHandlerRef = useRef();

  useEffect(() => {
    eventHandlerRef.current = eventHandler;
  });

  useEffect(() => {
    const handleEvent = (...args) => eventHandlerRef?.current(...args);

    eventTarget.addEventListener(eventName, handleEvent);

    return (() => eventTarget.removeEventListener(eventName, handleEvent));
  }, []);
}

使用钩子:

const currentValue = useSelector(state => getValue(state));

useEventHandler('my.custom.event', () => console.log(currentValue))

however, this results in the listener being removed/re-added every time the value of the selector changes, which isn't great.

为什么会出现问题?它会以任何可衡量的方式影响您的应用程序的性能吗?从不变性的角度来看,如果值发生变化,则事件侦听器会做一些不同的事情,因此将事件侦听器简单地替换为不同的侦听器是有意义的。

如果该值每秒更改多次(例如在每一帧或每次鼠标移动时),那么它确实会对性能造成小的影响,在这种情况下,Ori Drori 的解决方案可能是一个不错的解决方案,但它使代码更难理解,性能提升很小。

但如果该值每秒只更改几次或更少,我根本不会担心添加和删除事件侦听器。