Reactjs React Hooks:确保useEffect仅在自定义挂钩的数组参数的内容更改时运行的惯用方法

Reactjs React Hooks:确保useEffect仅在自定义挂钩的数组参数的内容更改时运行的惯用方法,reactjs,react-hooks,use-effect,Reactjs,React Hooks,Use Effect,我正在React中创建一个自定义钩子,它为给定的一组事件设置一个事件侦听器。存在一组默认的事件,在大多数用例中,该自定义挂钩的使用者不需要自定义这些事件。通常,我希望在安装组件时添加事件侦听器,在卸载组件时删除事件侦听器。但是,根据hooks的原则(以及eslint(react hooks/deps)lint规则),我希望优雅地处理对要观看的事件列表的更改。用React钩子实现这一点最惯用的方法是什么 假设我只想删除所有事件侦听器,并在事件列表更改时重新添加它们,我可以尝试以下操作: const

我正在React中创建一个自定义钩子,它为给定的一组事件设置一个事件侦听器。存在一组默认的事件,在大多数用例中,该自定义挂钩的使用者不需要自定义这些事件。通常,我希望在安装组件时添加事件侦听器,在卸载组件时删除事件侦听器。但是,根据hooks的原则(以及
eslint(react hooks/deps)
lint规则),我希望优雅地处理对要观看的事件列表的更改。用React钩子实现这一点最惯用的方法是什么

假设我只想删除所有事件侦听器,并在事件列表更改时重新添加它们,我可以尝试以下操作:

const useventwatcher=(
间隔=5000,
事件=['mousemove','keydown','wheel']
) => {
const timerIdRef=useRef();
useffect(()=>{
常量重置间隔=()=>{
if(timerIdRef.current){
清除间隔(timerIdRef.current);
}
timerIdRef.current=setInterval(()=>{
log(`${interval}毫秒,没有${events.join(',')}事件!`);
},间隔时间)
};
events.forEach(event=>window.addEventListener(event,resetInterval));
//不想错过第一次没有休息的时间
//给定的事件触发(因此无法在每次渲染后运行效果!)
重置间隔();
return()=>{
events.forEach(event=>window.removeEventListener(event,resetInterval));
};
},[事件,间隔];
}

不幸的是,这将无法发挥预期的作用。请注意,我想为
events
参数提供一个默认值。使用当前方法执行此操作意味着每次自定义挂钩运行时,
事件
都指向不同的引用,这意味着效果也会每次运行(由于浅层依赖关系比较)。理想情况下,我希望效果取决于数组的内容,而不是引用。实现这一点的最佳方法是什么?

您可以在两个不同的
useffects
中区分两种副作用

  • 您可以在加载的第一个
    useffect
    中运行初始
    resetInterval
    。 您需要运行一次,并且可以使用依赖项
    []

    但是,您需要在
    useffect
    之外提取
    resetInterval
    另一个问题是,现在每次渲染时都会重新创建
    resetInterval

    因此,您可以将其包装在
    useCallback

    第一个
    useffect
    取决于
    resetInterval
    (这会导致
    useffect
    在加载时运行一次,因此将调用
    resetInterval

  • 现在,您可以订阅第二个
    useffect
    中的所有
    事件
    ,依赖于
    [事件,间隔,重置间隔]
    ,如“eslint(react hooks/deps)lint规则”所建议的那样

  • 结果如下所示& 您可以在CodeSandbox上查看正在运行的演示。

    const useventwatcher=(
    间隔=2000,
    事件=[“mousemove”、“keydown”、“wheel”]
    ) => {
    const timerIdRef=useRef();
    const resetInterval=useCallback(()=>{
    if(timerIdRef.current){
    清除间隔(timerIdRef.current);
    }
    timerIdRef.current=setInterval(()=>{
    console.log(
    `通过了${interval}秒,没有${events.join(“,”}事件`
    );
    },间隔);
    },[事件,间隔];
    useffect(()=>{
    重置间隔();
    },[resetInterval]);
    useffect(()=>{
    events.forEach(event=>window.addEventListener(event,resetInterval));
    return()=>{
    events.forEach(event=>window.removeEventListener(event,resetInterval));
    };
    },[事件、间隔、重置间隔];
    };
    
    查看工作页面(出于演示目的,我将间隔设置为2秒)

    当您移动鼠标、滚轮或按键时,控制台日志将不会显示。一旦没有触发这些事件,您将看到控制台日志消息


    因此我们需要将
    事件
    包含到DEP中,但我们肯定不希望出现无休止的渲染循环

    选项1:使用
    JSON.stringify
    或类似方法将字符串作为依赖项而不是数组传递

    function useEventWatcher(events = ['click'])
    useEffect(() => {
    }, [JSON.stringifiy(events.sort())]) 
    
    但是,ESLint仍会抱怨,因此请抑制它或使用反字符串化:

    const eventsStringified = JSON.stringify(events.sort());
    useEffect(() => {
     const eventsUnstringified = JSON.parse(eventsStringified);
    }, [eventStringified]);
    
    选项2:将设置默认值移动到
    usemo
    。因此,当未传递
    事件
    参数时,默认值将参考相同(因此它是
    未定义的


    为什么要避免在deps中列出
    事件
    ?若用户最终想要指定事件,那个么将
    ['scroll']
    更改为
    ['click']
    处理错误的事件可能会让人大吃一惊。免费处理这样的案件不是更好吗?请参阅我的编辑以获得澄清。
    function useEventWatcher(events) {
    const eventsOrDefault = useMemo(() => eventsPassed || ['click'], [events]);
    
    useEffect(() => {
    }, [eventsOrDefault]);
    }