我一直在玩 React 16.7-alpha 中的新钩子系统,当我正在处理的状态是对象或数组时,在 useEffect 中陷入了无限循环。
首先,我使用 useState 并使用这样的空对象启动它:
const [obj, setObj] = useState({});
然后,在 useEffect 中,我再次使用 setObj 将其设置为空对象。作为第二个参数,我传递 [obj],希望如果对象的内容没有更改,它不会更新。但它一直在更新。我猜是因为无论内容如何,这些都是不同的对象,让 React 认为它在不断变化?
useEffect(() => {
setIngredients({});
}, [ingredients]);
数组也是如此,但作为一个原语,它不会像预期的那样陷入循环。
使用这些新的钩子,在检查天气内容是否发生变化时,我应该如何处理对象和数组?
useEffect
钩子中的依赖项
将空数组作为第二个参数传递给 useEffect 使其仅在挂载和卸载时运行,从而停止任何无限循环。
useEffect(() => {
setIngredients({});
}, []);
我在 https://www.robinwieruch.de/react-hooks/ 上关于 React 钩子的博文中澄清了这一点
有同样的问题。我不知道他们为什么不在文档中提到这一点。只是想在 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 将仅存储对该对象的引用并在引用更改时运行效果,这通常是每一次(我现在不知道如何)。
解决方案是传递对象中的值。你可以试试,
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]);
0
传递给依赖数组吗?
}, ...[Object.values(obj)]); ...
或简单地删除方括号 }, Object.values(obj)); ...
,第一个解决方案将起作用
如果您正在构建自定义钩子,有时可能会导致默认的无限循环,如下所示
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 = {}
}
如文档 (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
您可以将任何对象作为第二个参数传递。如果这个对象保持不变,你的效果只会在第一次坐骑后触发。如果对象发生变化,将再次触发效果。
你的无限循环是由于循环
useEffect(() => {
setIngredients({});
}, [ingredients]);
setIngredients({});
将更改 ingredients
的值(每次将返回一个新引用),它将运行 setIngredients({})
。要解决此问题,您可以使用任一方法:
将不同的第二个参数传递给 useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
setIngredients({});
}, [timeToChangeIngrediants ]);
setIngrediants
将在 timeToChangeIngrediants
更改时运行。
我不确定一旦更改成分,哪些用例证明更改成分是合理的。但如果是这种情况,您将 Object.values(ingrediants) 作为第二个参数传递给 useEffect。
useEffect(() => {
setIngredients({});
}, Object.values(ingrediants));
如果在 useEffect 末尾包含空数组:
useEffect(()=>{
setText(text);
},[])
它会运行一次。
如果您还包括数组上的参数:
useEffect(()=>{
setText(text);
},[text])
它会在文本参数更改时运行。
我不确定这是否适合您,但您可以尝试像这样添加 .length:
useEffect(() => {
// fetch from server and set as obj
}, [obj.length]);
在我的情况下(我正在获取一个数组!)它在装载时获取数据,然后再次仅在更改时获取数据并且它没有进入循环。
如果您使用此优化,请确保数组包含组件范围内的所有值(例如 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]);
最好的方法是使用 Lodash 中的 usePrevious() 和 _.isEqual() 将之前的值与当前值进行比较。导入 isEqual 和 useRef。将您之前的值与 useEffect() 中的当前值进行比较。如果它们相同,则不执行其他更新。 usePrevious(value) 是一个自定义钩子,它使用 useRef() 创建一个 ref。
下面是我的代码片段。我在使用 firebase hook 更新数据时遇到了无限循环的问题
导入 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' 导出函数 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]));
lodash
您还可以解构依赖数组中的对象,这意味着状态只会在对象的某些部分更新时更新。
为了这个例子,假设成分包含胡萝卜,我们可以将它传递给依赖项,并且只有当胡萝卜改变时,状态才会更新。
然后,您可以更进一步,仅在某些点更新胡萝卜的数量,从而控制状态何时更新并避免无限循环。
useEffect(() => {
setIngredients({});
}, [ingredients.carrots]);
当用户登录网站时,可以使用这样的东西的一个例子。当他们登录时,我们可以解构用户对象以提取他们的 cookie 和权限角色,并相应地更新应用程序的状态。
我的案例在遇到无限循环时很特别,场景是这样的:
我有一个对象,假设 objX 来自道具,我在道具中解构它,例如:
const { something: { somePropery } } = ObjX
我使用 somePropery
作为我的 useEffect
的依赖项,例如:
useEffect(() => {
// ...
}, [somePropery])
它导致我陷入无限循环,我试图通过将整个 something
作为依赖项传递来处理这个问题,并且它工作正常。
在将复杂对象作为状态并从 useRef
更新它时,我经常遇到无限重新渲染:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients({
...ingredients,
newIngedient: { ... }
});
}, [ingredients]);
在这种情况下,eslint(react-hooks/exhaustive-deps)
迫使我(正确地)将 ingredients
添加到依赖项数组中。但是,这会导致无限的重新渲染。与此线程中某些人所说的不同,这是正确的,并且您无法将 ingredients.someKey
或 ingredients.length
放入依赖项数组中。
解决方案是 setter 提供您可以参考的旧值。您应该使用它,而不是直接引用 ingredients
:
const [ingredients, setIngredients] = useState({});
useEffect(() => {
setIngredients(oldIngedients => {
return {
...oldIngedients,
newIngedient: { ... }
}
});
}, []);
我用于数组状态的另一个有效解决方案是:
useEffect(() => {
setIngredients(ingredients.length ? ingredients : null);
}, [ingredients]);
不定期副业成功案例分享