将数组传递给 useEffect 的依赖项列表

95

每隔5秒钟都会有一些数据从长轮询中返回,我希望我的组件在数组的任何一个项(或者数组长度本身)发生变化时都能够派发一个动作。当将数组作为useEffect的依赖项传递时,如何防止useEffect进入无限循环,但仍然能够在任何值更改时派发一些动作?

useEffect(() => {
  console.log(outcomes)
}, [outcomes])

其中 outcomes 是一个ID数组,例如 [123, 234, 3212]。数组中的项可能被替换或删除,因此数组的总长度可能会发生变化,但不一定会保持不变,因此将 outcomes.length 作为依赖项是不必要的。

outcomes 来自于 reselect 的自定义选择器:

const getOutcomes = createSelector(
  someData,
  data => data.map(({ outcomeId }) => outcomeId)
)

1
没有足够的上下文,请包含实际导致无限循环的代码。 - Asaf Aviv
2
useEffect(() => { console.log(outcomes) }, [...outcomes]) - hazardous
1
另一种选择是不可变列表。 - hazardous
6
展开数组是不好的。应用程序默认使用空数组启动,因此 useEffect 会在重新渲染之间抛出有关依赖项数量不同的错误。 - user0101
请创建一个可生产的示例,如何创建一个最小化、可复现的示例 - Dennis Vash
很遗憾,这些回答都无法在没有来自react-hooks/exhaustive-deps警告的情况下运行。 - ParkerD
9个回答

159

您可以将 JSON.stringify(outcomes) 作为依赖项列表传递:

在此处阅读更多信息:链接

useEffect(() => {
  console.log(outcomes)
}, [JSON.stringify(outcomes)])

26
我很确定这就是楼主想要的答案。这个想法并不是我发明的,而是来自于 Dan Abramov (React 核心团队成员)。如果有人在我的回答中发现问题,请告诉我具体是什么问题,这样我才能改进我的回答让它更好。 - Loi Nguyen Huynh
30
@LoiNguyenHuynh:我知道为什么答案可行,我认为这是一个好的答案,但是对于React团队,因为React是构建Web应用程序的惊人框架,我非常尊重...但是在useEffect / useMemo / useCallback中传递数组或对象的依赖项确实一直困扰着我,我开始厌倦使用解决方法。我们有没有办法讨论一些改进?我不知道从哪里开始。 - Patrick Da Silva
10
这会导致 ESLint 在 react-hooks/exhaustive-deps 规则下发出两个警告:一个是缺少依赖,另一个是复杂表达式。我不想忽略这两个警告,也许可以使用自定义的深度比较 hook 来解决问题,但我不知道 ESLint 是否能够识别自定义 hook。 - Vsevolod Golovanov
3
我们不应该使用 .sort() 然后再转换为字符串吗?如果项目改变但长度保持不变会怎样? - trainoasis
3
这不应该是答案。正如其他评论中所指出的那样,它并没有修复eslint警告。React团队多次表示,如果您需要禁用警告,则正在做错误的事情。 - ParkerD
显示剩余12条评论

18

如果您知道对象的形状,使用JSON.stringify()或任何深度比较方法可能效率低下。您可以编写自己的effect hook,根据自定义相等函数的结果触发回调。

useEffect通过检查依赖数组中的每个值是否与上一次渲染中的实例相同来工作。如果其中一个不同,则执行回调。因此,我们只需要使用useRef保留我们感兴趣的数据的实例,并仅在自定义相等检查返回false时分配新实例以触发effect。

function arrayEqual(a1: any[], a2: any[]) {
  if (a1.length !== a2.length) return false;
  for (let i = 0; i < a1.length; i++) {
    if (a1[i] !== a2[i]) {
      return false;
    }
  }
  return true;
}

type MaybeCleanUpFn = void | (() => void);

function useNumberArrayEffect(cb: () => MaybeCleanUpFn, deps: number[]) {
  const ref = useRef<number[]>(deps);

  if (!arrayEqual(deps, ref.current)) {
    ref.current = deps;
  }

  useEffect(cb, [ref.current]);
}

使用方法

function Child({ arr }: { arr: number[] }) {
  useNumberArrayEffect(() => {
    console.log("run effect", JSON.stringify(arr));
  }, arr);

  return <pre>{JSON.stringify(arr)}</pre>;
}

向前迈进一步,我们还可以通过创建一个接受自定义相等性函数的效果钩子来重复使用这个钩子。

type MaybeCleanUpFn = void | (() => void);
type EqualityFn = (a: DependencyList, b: DependencyList) => boolean;

function useCustomEffect(
  cb: () => MaybeCleanUpFn,
  deps: DependencyList,
  equal?: EqualityFn
) {
  const ref = useRef<DependencyList>(deps);

  if (!equal || !equal(deps, ref.current)) {
    ref.current = deps;
  }

  useEffect(cb, [ref.current]);
}

使用方法

useCustomEffect(
  () => {
    console.log("run custom effect", JSON.stringify(arr));
  },
  [arr],
  (a, b) => arrayEqual(a[0], b[0])
);

演示

编辑59467758 /将数组传递给useEffect依赖项列表


3
这是解决问题的正确方式(使用 useRef)。 - Ben Barkay
3
这个答案是错误的,useRef().current 不能在依赖数组中使用:https://medium.com/welldone-software/usecallback-might-be-what-you-meant-by-useref-useeffect-773bc0278ae - flying sheep
@flyingsheep 错误在于你的参数。你有没有阅读代码并尝试运行演示?ref.current是一个中间变量,用于存储新值,这会根据相等性检查来调用effect回调。触发重新渲染的是从外部传入的依赖项值,而不是ref.current - NearHuscarl
1
答案包含 useEffect(cb, [ref.current]),它等同于 useEffect(cb) - flying sheep
4
感谢 @NearHuscarl 提供全面的代码和说明,但我无法忍受为了那些应该开箱即用的东西而写这么多的代码,因此拒绝这样做。我会使用 JSON.stringify 并屏蔽警告。 - Luis Antonio Canettoli Ordoñez
显示剩余3条评论

11

另一个 ES6 选项是使用模板字符串将其转换为字符串。类似于 JSON.stringify(),但结果不会被包装在 [] 中。

useEffect(() => {
  console.log(outcomes)
}, [`${outcomes}`])

如果数组的大小不改变,另一种选择是使用展开语法

useEffect(() => {
  console.log(outcomes)
}, [ ...outcomes ])

7
请注意,如果您在数组中混合使用字符串和数字,则检查可能会失败,因为\${[1,2,3]}``${[1,2,'3']}``相同。但是,在同一数组中存储不同类型的值本身就是一个糟糕的想法。 - NearHuscarl
3
不幸的是,使用spread操作符会给我返回一个警告: React Hook useEffect在其依赖数组中使用了spread操作符。这意味着我们无法静态地验证您是否传递了正确的依赖项。 - Luis Antonio Canettoli Ordoñez
第一个与编写 outcomes.join() 相同。 - Steve Bennett

4
作为 loi-nguyen-huynh的回答的补充,对于遇到eslint exhaustive-deps 警告的任何人,可以通过首先将字符串化的JSON拆分成变量来解决此问题。
const depsString = JSON.stringify(deps);
React.useEffect(() => {
    ...
}, [depsString]);

3
你不能在钩子内部使用 deps 而不会收到不同的 exhaustive-deps 警告。但是,如果在钩子中 使用 depsString,也会收到警告。你的解决方案是在 effect 中使用 JSON.parse 吗?老实说,这似乎很荒谬。 - ParkerD

2
我建议您研究一下这个开源软件包,它是为解决您所描述的问题(深度比较依赖数组中的值而不是浅层比较)而创建的:

https://github.com/kentcdodds/use-deep-compare-effect

使用/API 与 useEffect 完全相同,但它将进行深度比较。

然而,我要警告你不要在不需要的地方使用它,因为这有可能导致性能下降,原因是进行了不必要的深度比较,而浅层比较就足够了。


0

快速解决方案,虽然有点像黑客技巧:

const [string, setString] = useState('1');

useEffect(() => {
 console.log(outcomes)
}, [string])

当你更新数组 'outcomes' 时,也要像这样更新字符串

setString(prev => `${prev}2`)

0
在我的情况下,我采用了以下方法来克服这种情况:我只是传递了一个包含数组的对象。
useEffect(() => {
  console.log(outcomes)
}, [{outcomes}])

创建一个像这样的新对象与不传递任何依赖项是一样的,它将运行每个渲染。 - undefined

0
我在这里提出的解决方案适用于某些情况。对于这类问题,我所做的是将变量进行记忆化处理,这样 useEffect 就不会无限触发。
请注意,这并不完全符合上述问题的要求,但我想这可能会对某些人有所帮助。
const roles = useMemo(() => [EntityRole.Client, EntityRole.Agency], []);

useEffect(() => {
  console.log(roles)
}, [roles])


0
为了解决linting问题,你可以在useEffect中解析字符串化的变量。
  const getNames = (names) => {
      // stingify names list first
      const nameStr = JSON.stringify(names);

      useEffect(() => {
          // use stingify variable so useEffect don't have dependency on names
          const nameList = JSON.parse(nameStr);
          console.log(nameList);
      }, [nameStr]);
  };

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