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

Posted

技术标签:

【中文标题】React Hooks:确保 useEffect 仅在自定义钩子的数组参数内容更改时运行的惯用方法【英文标题】:React Hooks: Idiomatic way to ensure that useEffect runs only when the contents of an array argument to the custom hook change 【发布时间】:2019-12-05 06:15:04 【问题描述】:

我在 React 中创建了一个自定义钩子,它为给定的一组事件设置了一个事件监听器。存在一组默认事件,并且此自定义钩子的使用者预计不会在大多数用例中自定义这些事件。通常,我希望在安装组件时添加事件侦听器,并在卸载组件时将其删除。然而,坚持钩子的原则(和eslint(react-hooks/exhaustive-deps) lint 规则),我希望优雅地处理对要观察的事件列表的更改。使用 React 钩子实现此目的最惯用的方法是什么?

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

const useEventWatcher = (
  interval = 5000,
  events = ['mousemove', 'keydown', 'wheel']
) => 
  const timerIdRef = useRef();

  useEffect(() => 
    const resetInterval = () => 
      if (timerIdRef.current) 
        clearInterval(timerIdRef.current);
      

      timerIdRef.current = setInterval(() =>  
        console.log(`$interval milliseconds passed with no $events.join(', ') events!`);
      , interval)
    ;

    events.forEach(event => window.addEventListener(event, resetInterval));

    // Don't want to miss the first time the interval passes without
    // the given events triggering (cannot run the effect after every render due to this!)
    resetInterval();

    return () => 
      events.forEach(event => window.removeEventListener(event, resetInterval));
    ;
  , [events, interval]);

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

【问题讨论】:

为什么要避免在部门中列出events?如果用户最终想要指定事件,那么将['scroll'] 更改为['click'] 处理错误事件可能会令人大吃一惊。免费处理这样的案子不是更好吗? 查看我的编辑以获得澄清。 【参考方案1】:

您可以在两个不同的useEffects 中分离两个副作用。

    您可以在加载时在第一个 useEffect 中运行初始 resetInterval。 您需要运行一次,您可能会使用 [] 的依赖项。 但是,您需要在useEffect 之外提取resetInterval。 另一个问题是,现在 resetInterval 在每次渲染期间都会重新创建。 所以你可以把它包装在useCallback. 第一个useEffect 依赖于resetInterval(这会导致useEffect 在加载时运行一次,因此将调用resetInterval

    现在您可以按照“eslint(react-hooks/exhaustive-deps) lint rule”的建议,在第二个 useEffect 中订阅所有 events 并依赖于 [events, interval, resetInterval]

    李>

结果如下所示 & 您可以在 CodeSandbox 上查看工作演示。

    const useEventWatcher = (
      interval = 2000,
      events = ["mousemove", "keydown", "wheel"]
    ) => 
      const timerIdRef = useRef();

      const resetInterval = useCallback(() => 
        if (timerIdRef.current) 
          clearInterval(timerIdRef.current);
        

        timerIdRef.current = setInterval(() => 
          console.log(
            `$interval seconds passed with no $events.join(", ") events!`
          );
        , interval);
      , [events, interval]);

      useEffect(() => 
        resetInterval();
      , [resetInterval]);

      useEffect(() => 
        events.forEach(event => window.addEventListener(event, resetInterval));

        return () => 
          events.forEach(event => window.removeEventListener(event, resetInterval));
        ;
      , [events, interval, resetInterval]);
    ;

查看工作页面(出于演示目的,我将间隔设置为 2 秒)

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

【讨论】:

【参考方案2】:

所以我们需要将 events 包含到 deps 中,但我们肯定不希望无休止的渲染循环。

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

function useEventWatcher(events = ['click'])
useEffect(() => 
, [JSON.stringifiy(events.sort())]) 

但是 ESLint 仍然会抱怨,所以要么抑制它,要么使用 de-stringify:

const eventsStringified = JSON.stringify(events.sort());
useEffect(() => 
 const eventsUnstringified = JSON.parse(eventsStringified);
, [eventStringified]);

选项#2:将设置默认值移动到useMemo。所以默认值在events参数没有被传递时引用相同(所以它是undefined

function useEventWatcher(events) 
const eventsOrDefault = useMemo(() => eventsPassed || ['click'], [events]);

useEffect(() => 
, [eventsOrDefault]);

【讨论】:

以上是关于React Hooks:确保 useEffect 仅在自定义钩子的数组参数内容更改时运行的惯用方法的主要内容,如果未能解决你的问题,请参考以下文章

React Hooks(useEffect) 表单子任务

React Hooks --- useState 和 useEffect

react源码debugger-各个hooks的逻辑实现(useState和useEffect)

Firebase 和 React Hooks(useState 和 useEffect)

如何使用 react-hooks (useEffect) 缓冲流数据以便能够一次更新另一个组件以避免多次重新渲染?

如何使用 React hooks 和 Redux 从 useEffect 执行 store.unsubscribe