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() 将先前值与当前值进行比较。 导入 isEqual 和 useRef。将您之前的值与 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函数从何而来? @JoshBowdenlodash
【参考方案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 中的无限循环
React hook useEffect 永远/无限循环连续运行