确定是哪个依赖数组变量导致了 useEffect 钩子的触发

240
有没有一种简单的方法来确定 useEffect 的依赖数组中哪个变量触发了函数的重新执行?
仅仅记录每个变量可能会产生误导,如果 a 是一个函数而 b 是一个对象,它们在记录时可能看起来相同,但实际上是不同的,从而导致 useEffect 重新执行。
例如:
React.useEffect(() => {
  // which variable triggered this re-fire?
  console.log('---useEffect---')
}, [a, b, c, d])

我目前的方法是逐个删除依赖变量,直到我注意到导致过多useEffect调用的行为,但肯定有更好的方法来缩小范围。


3
只是一个想法,如果您需要验证哪个变量发生了变化,那么拥有多个useEffect(每个独立可能发生变化的变量一个)是否有意义。因为很明显您正在尝试将两个用例合并成一个? - Archmede
@Archmede 如果所需的操作基本上都在一起,那听起来非常重复。 - Akaisteph7
10个回答

226

我最终从各种答案中选取了一些来创建自己的hook。我希望能够仅通过替换 useEffect 来快速调试哪个依赖项会触发 useEffect

const usePrevious = (value, initialValue) => {
  const ref = useRef(initialValue);
  useEffect(() => {
    ref.current = value;
  });
  return ref.current;
};
const useEffectDebugger = (effectHook, dependencies, dependencyNames = []) => {
  const previousDeps = usePrevious(dependencies, []);

  const changedDeps = dependencies.reduce((accum, dependency, index) => {
    if (dependency !== previousDeps[index]) {
      const keyName = dependencyNames[index] || index;
      return {
        ...accum,
        [keyName]: {
          before: previousDeps[index],
          after: dependency
        }
      };
    }

    return accum;
  }, {});

  if (Object.keys(changedDeps).length) {
    console.log('[use-effect-debugger] ', changedDeps);
  }

  useEffect(effectHook, dependencies);
};

下面是两个例子。对于每个示例,我假设dep2从“foo”更改为“bar”。示例1显示没有传递dependencyNames的输出结果,示例2显示了一个使用了dependencyNames参数的示例。

示例1

之前:

useEffect(() => {
  // useEffect code here... 
}, [dep1, dep2])

之后:

useEffectDebugger(() => {
  // useEffect code here... 
}, [dep1, dep2])

控制台输出:

{
  1: {
    before: 'foo',
    after: 'bar'
  }
}

对象键 '1' 表示发生更改的依赖项的索引。在这里,dep2 发生了更改,因为它是依赖项中的第二个项目,或索引1。

示例 2

之前:

useEffect(() => {
  // useEffect code here... 
}, [dep1, dep2])

之后:

useEffectDebugger(() => {
  // useEffect code here... 
}, [dep1, dep2], ['dep1', 'dep2'])

控制台输出:

{
  dep2: {
    before: 'foo',
    after: 'bar'
  }
}

8
React 可能会发出警告,提示 "React Hook useEffect has a missing dependency: 'effectHook'." 您可以通过将 effectHook 函数作为依赖项来处理此问题,只需将 useEffect(effectHook, dependencies); 更改为 useEffect(effectHook, [effectHook, ...dependencies]); - Josh Kautz
1
如果你遇到错误 TypeError: Cannot read properties of undefined (reading '0'),请将所有的 previousDeps 实例替换为 previousDeps?.(这是 optional chaining)。 - warren wiser
如果你得到以下提示:没有匹配的重载。 在2个重载中,'(o:{}):string []',出现了以下错误。 类型“unknown”的参数无法分配给类型“{}” 的参数。 在2个重载中,'(o:object):string []',出现了以下错误。 类型“unknown”的参数无法分配给类型“object”。' ,请将Object.keys(changedDeps)更改为Object.keys(changedDeps as any[])`。 - Nathan Tew

64

@simbathesailor/use-what-changed 轻松易用!

  1. 使用 npm/yarn 安装,并添加 --dev--no-save 参数

  2. 导入:

  3. import { useWhatChanged } from '@simbathesailor/use-what-changed';
    
  4. 称它为:

  5. // (guarantee useEffect deps are in sync with useWhatChanged)
    let deps = [a, b, c, d]
    
    useWhatChanged(deps, 'a, b, c, d');
    useEffect(() => {
      // your effect
    }, deps);
    

在控制台中创建了这个漂亮的图表:

从Github加载的图像

有两个常见的罪魁祸首:

  1. 传递对象的方式如下:
// Being used like:
export function App() {
  return <MyComponent fetchOptions={{
    urlThing: '/foo',
    headerThing: 'FOO-BAR'
  })
}
export const MyComponent = ({fetchOptions}) => {
  const [someData, setSomeData] = useState()
  useEffect(() => {
    window.fetch(fetchOptions).then((data) => {
      setSomeData(data)
    })

  }, [fetchOptions])

  return <div>hello {someData.firstName}</div>
}

如果可以,对象情况的解决方法是将静态对象从组件渲染中分离出来:

const fetchSomeDataOptions = {
  urlThing: '/foo',
  headerThing: 'FOO-BAR'
}
export function App() {
  return <MyComponent fetchOptions={fetchSomeDataOptions} />
}

你也可以使用 useMemo 包装:

export function App() {
  return <MyComponent fetchOptions={
    useMemo(
      () => {
        return {
          urlThing: '/foo',
          headerThing: 'FOO-BAR',
          variableThing: hash(someTimestamp)
        }
      },
      [hash, someTimestamp]
    )
  } />
}

函数的情况类似,但是你可能会得到陈旧的闭包。


3
(点表示值未更改。绿色勾号表示已更改。)甚至还有一个 Babel 插件(认真地去给这个家伙的项目点赞!)https://github.com/simbathesailor/use-what-changed - Devin Rhode
不知道为什么,但是它对我没有记录任何东西。 - Jamil Alisgenderov
2
@JamilAlisgenderov 我认为 useWhatChanged 必须使用 console.table.. 因此,如果您正在尝试在不支持 console.table 的旧浏览器中进行测试,您可以检查是否定义了 console.table。您还可以验证 useEffect 钩子内部的常规 console.log('something changed', 'table defined?', !!console.table); 是否记录。否则... 可能需要在 github 上提交一个问题,并注明您的 react 版本+浏览器信息。 - Devin Rhode
@JamilAlisgenderov 你最后弄清楚 use-what-changed 没有记录任何内容的原因了吗? - Devin Rhode
似乎在Storybook中不受支持。 - Vinujan.S
这个库的建议非常棒。 - jsaddwater

18

更新

在实际使用中,我目前喜欢以下解决方案,它借鉴了 Retsam 的某些方面:

const compareInputs = (inputKeys, oldInputs, newInputs) => {
  inputKeys.forEach(key => {
    const oldInput = oldInputs[key];
    const newInput = newInputs[key];
    if (oldInput !== newInput) {
      console.log("change detected", key, "old:", oldInput, "new:", newInput);
    }
  });
};
const useDependenciesDebugger = inputs => {
  const oldInputsRef = useRef(inputs);
  const inputValuesArray = Object.values(inputs);
  const inputKeysArray = Object.keys(inputs);
  useMemo(() => {
    const oldInputs = oldInputsRef.current;

    compareInputs(inputKeysArray, oldInputs, inputs);

    oldInputsRef.current = inputs;
  }, inputValuesArray); // eslint-disable-line react-hooks/exhaustive-deps
};

然后可以通过复制一个依赖数组字面量并将其更改为对象字面量来使用:

useDependenciesDebugger({ state1, state2 });

这样可以使日志记录器知道变量的名称,无需单独参数。

编辑useDependenciesDebugger


我也喜欢这个答案。与我的答案相比,它需要更多的工作来设置,但会提供更好的输出,因为每个依赖项都有一个名称,而我的只是说哪个索引发生了变化。 - Retsam
如果你想在值改变时记录旧值和新值,你可以从一个持有 truefalse 的引用切换到一个持有 null{prevValue: value} 的引用。 - Retsam

9
据我所知,目前没有现成的简单方法来完成这个任务,但您可以添加一个自定义钩子来跟踪其依赖项,并记录哪个依赖项发生了更改:
// Same arguments as useEffect, but with an optional string for logging purposes
const useEffectDebugger = (func, inputs, prefix = "useEffect") => {
  // Using a ref to hold the inputs from the previous run (or same run for initial run
  const oldInputsRef = useRef(inputs);
  useEffect(() => {
    // Get the old inputs
    const oldInputs = oldInputsRef.current;

    // Compare the old inputs to the current inputs
    compareInputs(oldInputs, inputs, prefix)

    // Save the current inputs
    oldInputsRef.current = inputs;

    // Execute wrapped effect
    func()
  }, inputs);
};
compareInputs部分可以看起来像这样:
const compareInputs = (oldInputs, newInputs, prefix) => {
  // Edge-case: different array lengths
  if(oldInputs.length !== newInputs.length) {
    // Not helpful to compare item by item, so just output the whole array
    console.log(`${prefix} - Inputs have a different length`, oldInputs, newInputs)
    console.log("Old inputs:", oldInputs)
    console.log("New inputs:", newInputs)
    return;
  }

  // Compare individual items
  oldInputs.forEach((oldInput, index) => {
    const newInput = newInputs[index];
    if(oldInput !== newInput) {
      console.log(`${prefix} - The input changed in position ${index}`);
      console.log("Old value:", oldInput)
      console.log("New value:", newInput)
    }
  })
}

你可以像这样使用它:
useEffectDebugger(() => {
  // which variable triggered this re-fire?
  console.log('---useEffect---')
}, [a, b, c, d], 'Effect Name')

您将获得类似以下的输出:

Effect Name - The input changed in position 2
Old value: "Previous value"
New value: "New value"

4

6
将另一个StackOverflow主题的链接发布出来可能会很有用。 - jknotek
仅提供链接的答案在 SO 上并不被视为有价值的。请查看 [答案]。 - isherwood

2

我认为我更喜欢在OP评论中提到的那个,只需为每个单独的依赖项复制粘贴一个useEffect。非常容易理解,没有新的、复杂的代码/库出现的风险。

      . // etc in big hook
      .
      return complicated(thing);
    },
    [api, error, needsSeqs, throttle]
  );

  useEffect(() => console.log('api'), [api]);
  useEffect(() => console.log('error'), [error]);
  useEffect(() => console.log('needsSeqs'), [needsSeqs]);
  useEffect(() => console.log('throttle'), [throttle]);

1

React beta文档建议这些步骤

  • 使用console.log记录您的依赖项数组:
  const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]);
  console.log([todos, tab]);
  • 在控制台中右键单击来自不同重新渲染的数组,然后选择“存储为全局变量”以便两者都可以访问。如果您处于严格模式下,则可能很重要不要比较两个连续的数组,但我不确定。
  • 比较每个依赖项:
  Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?

0

这个问题已经有几个好的、可行的答案了,但我不喜欢它们中的任何一个的 DX。

所以我写了一个库,记录最容易使用的更改依赖项的方式,并添加了一个函数来记录两个对象之间的深度比较,这样你就可以知道你的对象内部发生了什么变化。

我称它为:react-what-changed

自述文件中包含了所有你需要的示例。

使用非常简单明了:

npm install react-what-changed --save-dev

import { reactWhatChanged as RWC } from 'react-what-changed';

function MyComponent(props) {
  useEffect(() => {
    someLogic();
  }, RWC([somePrimitive, someArray, someObject]));
}

在这个包中,您还将找到两个有用的函数,用于打印对象之间的深度比较(仅差异)。例如:
import { reactWhatDiff as RWD } from 'react-what-changed';

function MyComponent(props) {
  useEffect(() => {
    someLogic();
  }, [somePrimitive, someArray, someObject]);

  RWD(someArray);
}

0

我在@Bradley的答案上进行了扩展,以便可以与useMemouseCallback一起使用。我相当确定它可以工作,尽管我没有进行彻底测试。请注意,它们都返回它们的hooks,因为useMemouseCallback需要返回值。我没有看到以这种方式返回useEffect会产生任何不良影响。如果您发现问题或修复,请随时评论。

import React, { useEffect, useRef } from 'react'

const usePrevious = (value, initialValue) => {
  const ref = useRef(initialValue)
  useEffect(() => {
    ref.current = value
  })
  return ref.current
}

const hookTypes = ['Effect', 'Memo', 'Callback'] as const
export const debuggers = hookTypes.reduce(
  (obj, hookType) => ({
    ...obj,
    [`use${hookType}Debugger`]: (
      effectHook,
      dependencies,
      dependencyNames = [],
      hookName = ''
    ) => {
      const previousDeps = usePrevious(dependencies, [])

      const changedDeps = dependencies.reduce((accum, dependency, index) => {
        if (dependency !== previousDeps[index]) {
          const keyName = dependencyNames[index] || index
          return {
            ...accum,
            [keyName]: {
              before: previousDeps[index],
              after: dependency,
            },
          }
        }

        return accum
      }, {} )

      if (Object.keys(changedDeps).length) {
        console.log(`[use-${hookType.toLowerCase()}-debugger] `, hookName, changedDeps)
      }

      // @ts-ignore
      return React[`use${hookType}`](effectHook, dependencies)
    },
  }),
  {} as Record<`use${typeof hookTypes[number]}Debugger`, (
    effectHook: Function,
    dependencies: Array<any>,
    dependencyNames?: string[],
    hookName?: string
  ) => any>
)

0
我遇到了同样的问题,我的useEffect运行了多次。为了找出是什么原因导致这种情况,我在开头加入了一个日志语句,以便了解它运行了多少次,然后我逐个删除依赖项,并观察console.log运行了多少次。所以,当日志语句的数量减少时,就是你删除的那个依赖项导致了useEffect的多次执行。
useEffect(() => {
console.log('I ran'); // Log statement to keep track of how many times useEffect is running
// your stuff
}, [d1, d2, d3, d4]); //Dependancies

所以,逐个删除这些依赖项,并跟踪 console.log。如果数字减少了,那么该依赖项就是导致问题的原因。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接