我一直在 React 16.7-alpha 中使用新的钩子系统,当我处理的状态是对象或数组时,我在 useEffect 中陷入了无限循环。
首先,我使用 useState 并使用如下所示的空对象启动它:
const [obj, setObj] = useState({});
然后,在useEffect中,我使用setObj再次将其设置为空对象。作为第二个参数,我传递 [obj],希望如果对象的内容没有更改,它不会更新。但它一直在更新。我想是因为无论内容如何,这些总是不同的对象,让 React 认为它会不断变化?
useEffect(() => {
setIngredients({});
}, [ingredients]);
数组也是如此,但作为原语,它不会像预期的那样陷入循环。
使用这些新钩子,在检查天气内容是否发生变化时,我应该如何处理对象和数组?
将空数组作为第二个参数传递给 useEffect 使其仅在挂载和卸载时运行,从而停止任何无限循环。
useEffect(() => {
setIngredients({});
}, []);
这在 React 钩子的博客文章中向我澄清了 https://www.robinwieruch.de/react-hooks/
有同样的问题。我不知道为什么他们不在文档中提到这一点。只是想给托比亚斯·豪根的答案补充一点。
要在每次组件/父重新渲染中运行,您需要使用:
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
}, [] )
要在组件挂载和数据/数据2更改上运行任何内容:
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 将只存储对该对象的引用,并在引用更改时运行效果,这通常是每个单独的时间(我现在不知道如何)。
解决方案是在对象中传递值。你可以试试,
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]);
如果您正在构建自定义钩子,有时可能会导致默认值的无限循环,如下所示
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 = {}
}
你的无限循环是由于循环性
useEffect(() => {
setIngredients({});
}, [ingredients]);
setIngredients({});
将更改ingredients
的值(每次都会返回一个新引用),这将setIngredients({})
运行。要解决此问题,您可以使用任一方法:
- 传递不同的第二个参数以使用
const timeToChangeIngrediants = .....
useEffect(() => {
setIngredients({});
}, [timeToChangeIngrediants ]);
setIngrediants
将在timeToChangeIngrediants
更改时运行。
- 我不确定一旦更改了更改,什么用例就可以证明更改成分是合理的。但如果是这种情况,您可以将
Object.values(ingrediants)
作为第二个参数传递给 useEffect。
useEffect(() => {
setIngredients({});
}, Object.values(ingrediants));
如文档 (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
您可以将任何对象作为第二个参数传递。如果此对象保持不变,则仅在首次装载后触发效果。如果对象发生变化,将再次触发效果。
我不确定这是否适合您,但您可以尝试像这样添加 .length:
useEffect(() => {
// fetch from server and set as obj
}, [obj.length]);
在我的情况下(我正在获取一个数组!),它在装载时获取数据,然后仅在更改时再次获取数据,并且它没有进入循环。
如果在useEffect结束时包含空数组:
useEffect(()=>{
setText(text);
},[])
它会运行一次。
如果在数组上也包含参数:
useEffect(()=>{
setText(text);
},[text])
每当文本参数更改时,它都会运行。
当将复杂对象作为状态并从useRef
更新它时,我经常会遇到无限的重新渲染:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients({
...ingredients,
newIngedient: { ... }
});
}, [ingredients]);
在这种情况下,eslint(react-hooks/exhaustive-deps)
强制我(正确)将ingredients
添加到依赖项数组中。但是,这会导致无限的重新渲染。与此线程中的某些人所说的不同,这是正确的,您无法将ingredients.someKey
或ingredients.length
放入依赖项数组中。
解决方案是资源库提供您可以引用的旧值。你应该使用它,而不是直接引用ingredients
:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients(oldIngedients => {
return {
...oldIngedients,
newIngedient: { ... }
}
});
}, []);
<</div>
div class="one_answers">如果使用此优化,请确保数组包含组件范围(如 props 和状态)中随时间变化并由效果使用的所有值。
我相信他们正试图表达人们可能使用陈旧数据的可能性,并意识到这一点。我们在第二个参数的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]);
主要问题是 useEffect 将传入值与当前值浅层比较。这意味着这两个值使用"==="比较进行比较,该比较仅检查对象引用,尽管数组和对象值相同,但它将它们视为两个不同的对象。我建议您查看我关于 useEffect 作为生命周期方法的文章。
最好的方法是使用UsePrevious() 和_.isEqual()将以前的值与当前值进行比较。导入是相等的和使用引用。将您以前的值与useEffect()中的当前值进行比较。如果它们相同,则不进行任何其他更新。usePrevious(value) 是一个自定义钩子,它使用useRef()创建一个ref。
下面是我的代码片段。我遇到了使用火碱钩更新数据的无限循环问题
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()
}
})
如果您确实需要比较对象并且当它更新时,这里有一个用于比较的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]));
您还可以在依赖项数组中解构对象,这意味着状态仅在对象的某些部分更新时更新。
为了这个例子,假设成分含有胡萝卜,我们可以将其传递给依赖项,并且只有当胡萝卜发生变化时,状态才会更新。
然后,您可以更进一步,仅在特定点更新胡萝卜的数量,从而控制状态何时更新并避免无限循环。
useEffect(() => {
setIngredients({});
}, [ingredients.carrots]);
可以使用此类内容的一个例子是用户登录网站时。当他们登录时,我们可以解构用户对象以提取其 cookie 和权限角色,并相应地更新应用程序的状态。
我的案例在遇到无限循环时很特别,Senario 是这样的:
我有一个对象,假设objX来自道具,我正在将其解构为道具,例如:
const { something: { somePropery } } = ObjX
我将somePropery
用作对useEffect
的依赖关系,例如:
useEffect(() => {
// ...
}, [somePropery])
它给我带来了无限循环,我试图通过将整个something
作为依赖项传递来处理这个问题,并且它工作正常。
我用于数组状态的另一个有效解决方案是:
useEffect(() => {
setIngredients(ingredients.length ? ingredients : null);
}, [ingredients]);