useEffect 中的无限循环

Posted

技术标签:

【中文标题】useEffect 中的无限循环【英文标题】:Infinite loop in useEffect 【发布时间】:2019-04-03 21:19:15 【问题描述】:

我一直在使用 React 16.7-alpha 中的新钩子系统,当我正在处理的状态是对象或数组时,我陷入了 。

首先,我使用 useState 并使用这样的空对象启动它:

const [obj, setObj] = useState();

然后,在 useEffect 中,我再次使用 setObj 将其设置为空对象。作为第二个参数,我传递 [obj],希望如果对象的 content 没有更改,它不会更新。但它一直在更新。我猜是因为无论内容如何,​​这些总是不同的对象,让 React 认为它在不断变化?

useEffect(() => 
  setIngredients();
, [ingredients]);

数组也是如此,但作为原语,它不会像预期的那样陷入循环。

使用这些新的钩子,当检查天气内容是否发生变化时,我应该如何处理对象和数组?

【问题讨论】:

Tobias,什么用例需要改变成分的值,一旦它的值改变了? @Tobias,您应该阅读我的回答。我相信你会接受它作为正确答案。 我读了这个article,它帮助我更清楚地理解了一些东西。我要做的是查找有关对象/数组的特定属性,例如元素的数量或名称(无论您喜欢什么),并将它们用作 useEffect 钩子中的依赖项 【参考方案1】:

将空数组作为第二个参数传递给 useEffect 使其仅在挂载和卸载时运行,从而停止任何无限循环。

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

在https://www.robinwieruch.de/react-hooks/https://www.robinwieruch.de/react-hooks/React hooks 的博文中向我澄清了这一点

【讨论】:

它实际上是一个空数组,而不是你应该传入的空对象。 这并不能解决问题,您需要传递钩子使用的依赖项 请注意,在卸载时,如果您指定了清理功能,效果将运行清理功能。实际效果不会在卸载时运行。 reactjs.org/docs/hooks-effect.html#example-using-hooks-1 当 setIngrediets 是依赖项时使用空数组是 Dan Abramov 所说的反模式。不要将 useEffectct 视为 componentDidMount() 方法 由于上面的人没有提供如何正确解决这个问题的链接或资源,我建议迎面而来的鸭子们检查一下,因为它解决了我的无限循环问题:***.com/questions/56657907/…跨度> 【参考方案2】:

遇到了同样的问题。我不知道他们为什么不在文档中提到这一点。只是想在 Tobias Haugen 的回答中添加一点。

要在每个组件/父级重新渲染中运行,您需要使用:

  useEffect(() => 

    // don't know where it can be used :/
  )

要在组件安装后只运行一次(将呈现一次),您需要使用:

  useEffect(() => 

    // do anything only one time if you pass empty array []
    // keep in mind, that component will be rendered one time (with default values) before we get here
  , [] )

在组件装载和 data/data2 上运行一次更改:

  const [data, setData] = useState(false)
  const [data2, setData2] = useState('default value for first render')
  useEffect(() => 

// if you pass some variable, than component will rerender after component mount one time and second time if this(in my case data or data2) is changed
// if your data is object and you want to trigger this when property of object changed, clone object like this let clone = JSON.parse(JSON.stringify(data)), change it clone.prop = 2 and setData(clone).
// if you do like this 'data.prop=2' without cloning useEffect will not be triggered, because link to data object in momory doesn't changed, even if object changed (as i understand this)
  , [data, data2] )

我大部分时间是如何使用它的:

export default function Book(id)  
  const [book, bookSet] = useState(false) 

  const loadBookFromServer = useCallback(async () => 
    let response = await fetch('api/book/' + id)
    response  = await response.json() 
    bookSet(response)
  , [id]) // every time id changed, new book will be loaded

  useEffect(() => 
    loadBookFromServer()
  , [loadBookFromServer]) // useEffect will run once and when id changes


  if (!book) return false //first render, when useEffect did't triggered yet we will return false

  return <div>JSON.stringify(book)</div>  

【讨论】:

根据 React 常见问题解答,it is not safe to omit functions from the list of dependencies。 @endavid 取决于你使用的道具是什么 当然,如果你不使用组件范围内的 any 值,那么省略是安全的。但是从 vue 的纯粹设计/架构角度来看,这不是一个好习惯,因为如果您需要使用道具,它需要您将整个函数移动到效果内,并且您最终可能会得到一个将使用的 useEffect 方法数量惊人的代码。 这真的很有帮助,在 useEffect 钩子中调用异步函数只会在值变化时更新。谢谢老哥 @egdavid 那么当值更新时运行某个代码块的正确方法是什么?使用 UseEffect 会导致 lint 错误将值包含在依赖项中,但这会导致无限循环,因此无法使用该方法【参考方案3】:

我也遇到过同样的问题,我通过确保在第二个参数[] 中传递原始值来解决它。

如果你传递一个对象,React 将只存储对该对象的引用并在引用更改时运行效果,这通常是每一次(但我现在不知道如何)。

解决方案是传递对象中的值。你可以试试,

const obj =  keyA: 'a', keyB: 'b' 

useEffect(() => 
  // do something
, [Object.values(obj)]);

const obj =  keyA: 'a', keyB: 'b' 

useEffect(() => 
  // do something
, [obj.keyA, obj.keyB]);

【讨论】:

另一种方法是使用 useMemo 创建这样的值,这样会保留引用并且依赖项的值被评估为相同 @helado 您可以使用 useMemo() 获取值或使用 useCallback() 获取函数 是否将随机值传递给依赖数组?当然,这可以防止无限循环,但这是鼓励吗?我也可以将0 传递给依赖数组吗? @Dinesh 如果您添加扩展运算符, ...[Object.values(obj)]); ... 或简单地删除方括号, Object.values(obj)); ...,第一个解决方案将起作用【参考方案4】:

如果您正在构建自定义钩子,有时可能会导致无限循环,默认情况如下

function useMyBadHook(values = ) 
    useEffect(()=>  
           /* This runs every render, if values is undefined */
        ,
        [values] 
    )

解决方法是使用相同的对象,而不是在每个函数调用时创建一个新对象:

const defaultValues = ;
function useMyBadHook(values = defaultValues) 
    useEffect(()=>  
           /* This runs on first call and when values change */
        ,
        [values] 
    )

如果您在组件代码中遇到此问题,如果您使用 defaultProps 而不是 ES6 默认值,则循环可能会得到修复

function MyComponent(values) 
  useEffect(()=>  
       /* do stuff*/
    ,[values] 
  )
  return null; /* stuff */


MyComponent.defaultProps = 
  values = 

【讨论】:

谢谢!这对我来说并不明显,而正是我遇到的问题。【参考方案5】:

如文档 (https://reactjs.org/docs/hooks-effect.html) 中所述,useEffect 钩子旨在在您希望在每次渲染后执行某些代码时使用。来自文档:

useEffect 是否在每次渲染后运行?是的!

如果您想对此进行自定义,您可以按照稍后出现在同一页面 (https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects) 中的说明进行操作。基本上,useEffect 方法接受 第二个参数,React 将检查该参数以确定是否必须再次触发效果。

useEffect(() => 
  document.title = `You clicked $count times`;
, [count]); // Only re-run the effect if count changes

您可以将任何对象作为第二个参数传递。 如果这个对象保持不变,你的效果只会在第一次安装后触发。如果对象发生变化,会再次触发效果。

【讨论】:

【参考方案6】:

你的无限循环是由于循环

useEffect(() => 
  setIngredients();
, [ingredients]);

setIngredients(); 将改变ingredients 的值(每次都会返回一个新的引用),它将运行setIngredients()。要解决这个问题,您可以使用任何一种方法:

    将不同的第二个参数传递给 useEffect
const timeToChangeIngrediants = .....
useEffect(() => 
  setIngredients();
, [timeToChangeIngrediants ]);

setIngrediants 将在 timeToChangeIngrediants 更改时运行。

    我不确定一旦更改成分,哪些用例证明更改成分是合理的。但如果是这种情况,您将 Object.values(ingrediants) 作为第二个参数传递给 useEffect。
useEffect(() => 
  setIngredients();
, Object.values(ingrediants));

【讨论】:

【参考方案7】:

如果您在 useEffect 的末尾包含空数组:

useEffect(()=>
        setText(text);
,[])

它会运行一次。

如果你在数组中也包含参数:

useEffect(()=>
            setText(text);
,[text])

它会在文本参数更改时运行。

【讨论】:

如果我们在钩子的末尾放置一个空数组,为什么它只会运行一次? useEffect 末尾的空数组是开发人员有目的的实现,用于在您可能需要在 useEffect 内部设置状态的情况下停止无限循环。否则会导致 useEffect -> state update -> useEffect -> 无限循环。 空数组会导致 eslinter 发出警告。 这里有一个详细的解释:reactjs.org/docs/…【参考方案8】:

我不确定这是否适合您,但您可以尝试像这样添加 .length:

useEffect(() => 
        // fetch from server and set as obj
, [obj.length]);

在我的情况下(我正在获取一个数组!)它在装载时获取数据,然后再次仅在更改时获取数据并且它没有进入循环。

【讨论】:

如果数组中的某个项目被替换了怎么办?在这种情况下,数组的长度将相同,并且效果不会运行!【参考方案9】:

如果您使用此优化,请确保数组包含组件范围内的所有值(例如 props 和 state),这些值随时间变化并被效果器使用。

我相信他们试图表达人们可能使用过时数据的可能性,并意识到这一点。我们在array 中为第二个参数发送的值的类型并不重要,只要我们知道如果这些值中的任何一个发生更改,它将执行效果。如果我们在效果中使用ingredients 作为计算的一部分,我们应该将它包含在array 中。

const [ingredients, setIngredients] = useState();

// This will be an infinite loop, because by shallow comparison ingredients !==  
useEffect(() => 
  setIngredients();
, [ingredients]);

// If we need to update ingredients then we need to manually confirm 
// that it is actually different by deep comparison.

useEffect(() => 
  if (is(<similar_object>, ingredients) 
    return;
  
  setIngredients(<similar_object>);
, [ingredients]);

【讨论】:

【参考方案10】:

主要问题是 useEffect 将传入值与当前值shallowly 进行比较。这意味着这两个值使用“===”比较进行比较,该比较仅检查对象引用,尽管数组和对象值相同,但它会将它们视为两个不同的对象。我建议您查看我的 article 关于 useEffect 作为生命周期方法的信息。

【讨论】:

【参考方案11】:

最好的方法是使用 Lodash 中的 usePrevious()_.isEqual() 将先前值与当前值进行比较。 导入 isEqualuseRef。将您之前的值与 useEffect() 中的当前值进行比较。如果它们相同,则不执行其他更新。 usePrevious(value) 是一个自定义钩子,它使用 useRef() 创建一个 ref

下面是我的代码的 sn-p。我在使用 firebase hook 更新数据时遇到了无限循环的问题

import React,  useState, useEffect, useRef  from 'react'
import 'firebase/database'
import  Redirect  from 'react-router-dom'
import  isEqual  from 'lodash'
import 
  useUserStatistics
 from '../../hooks/firebase-hooks'

export function TMDPage( match, history, location ) 
  const usePrevious = value => 
    const ref = useRef()
    useEffect(() => 
      ref.current = value
    )
    return ref.current
  
  const userId = match.params ? match.params.id : ''
  const teamId = location.state ? location.state.teamId : ''
  const [userStatistics] = useUserStatistics(userId, teamId)
  const previousUserStatistics = usePrevious(userStatistics)

  useEffect(() => 
      if (
        !isEqual(userStatistics, previousUserStatistics)
      ) 
        
        doSomething()
      
     
  )

【讨论】:

我猜人们对此投了反对票,因为您建议使用第三方库。但是,基本的想法是好的。 这不是还是会造成无限循环(虽然body几乎是空的),这样浏览器会吃掉很多CPU周期吗? @AlwaysLearning 你有更好的解决方案吗?【参考方案12】:

如果您确实需要比较对象并且当它被更新时,这里是一个deepCompare 钩子用于比较。公认的答案肯定没有解决这个问题。如果您需要效果在挂载时仅运行一次,则使用 [] 数组是合适的。

此外,其他投票答案仅通过执行obj.value 或类似于首先到达未嵌套的级别来解决对原始类型的检查。对于深度嵌套的对象,这可能不是最好的情况。

所以这是一个适用于所有情况的方法。

import  DependencyList  from "react";

const useDeepCompare = (
    value: DependencyList | undefined
): DependencyList | undefined => 
    const ref = useRef<DependencyList | undefined>();
    if (!isEqual(ref.current, value)) 
        ref.current = value;
    
    return ref.current;
;

你可以在useEffect钩子中使用相同的

React.useEffect(() => 
        setState(state);
    , useDeepCompare([state]));

【讨论】:

isEqual函数从何而来? @JoshBowden lodash【参考方案13】:

您还可以解构依赖数组中的对象,这意味着状态只会在对象的某些部分更新时更新。

为了这个例子,假设成分包含胡萝卜,我们可以将它传递给依赖项,只有当胡萝卜改变时,状态才会更新。

然后您可以更进一步,仅在某些点更新胡萝卜的数量,从而控制状态何时更新并避免无限循环。

useEffect(() => 
  setIngredients();
, [ingredients.carrots]);

当用户登录网站时,可以使用类似的方法。当他们登录时,我们可以解构用户对象以提取他们的 cookie 和权限角色,并相应地更新应用程序的状态。

【讨论】:

【参考方案14】:

我的Case特别遇到死循环,场景是这样的:

我有一个对象,假设 objX 来自道具,我在道具中解构它,例如:

const  something:  somePropery   = ObjX

我使用somePropery 作为我的useEffect 的依赖项,例如:


useEffect(() => 
  // ...
, [somePropery])

它导致我陷入无限循环,我试图通过将整个 something 作为依赖项传递来处理这个问题,并且它工作正常。

【讨论】:

【参考方案15】:

我用于数组状态的另一个有效解决方案是:

useEffect(() => 
  setIngredients(ingredients.length ? ingredients : null);
, [ingredients]);

【讨论】:

以上是关于useEffect 中的无限循环的主要内容,如果未能解决你的问题,请参考以下文章

GraphQL 默认数据导致 useEffect 中的无限循环

关于useEffect中的无限循环

当我想检查窗口对象的属性是不是在这里时,useEffect 中的无限循环

React hook useEffect 永远/无限循环连续运行

React useEffect 无限循环获取数据 axios

React - useEffect() 中的错误。 dat 是不是会使页面上的所有内容陷入无限循环?