无限循环的使用效果

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

首先,我使用 useState 并用一个空对象来启动它,如下所示:

const [obj, setObj] = useState({});

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

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

对于数组也是如此,但是作为一个原语,它不会像预期的那样卡在循环中。

使用这些新的钩子,我应该如何处理对象和数组时,检查天气的内容已经改变或没有?

182603 次浏览

传递一个空数组作为 useEffect 的第二个参数使它只能在 mount 和 unmount 上运行,从而停止任何无限循环。

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

我在 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
}, [] )

要运行 一次在组件挂载和 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]);

正如文档(https://reactjs.org/docs/hooks-effect.html)中所说,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(() => {
// fetch from server and set as obj
}, [obj.length]);

在我的例子中(我正在获取一个数组!)它在 mount 上获取数据,然后只在更改时获取数据,并且没有进入循环。

你的无限循环是由于循环

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

setIngredients({});将更改 ingredients的值(每次返回一个新的引用) ,该值将运行 setIngredients({})。要解决这个问题,你可以采用以下两种方法:

  1. 传递一个不同的第二个参数到 useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
setIngredients({});
}, [timeToChangeIngrediants ]);

timeToChangeIngrediants改变时,setIngrediants将运行。

  1. 我不知道什么样的用例可以证明一旦改变了就可以改变成分。但是,如果是这种情况,则将 Object.values(ingrediants)作为 useEffect 的第二个参数传递。
useEffect(() => {
setIngredients({});
}, Object.values(ingrediants));

如果使用此优化,请确保数组包含组件范围(如道具和状态)中随时间变化的所有值以及效果所使用的值。

我相信他们是想表达一种可能性,即人们可能正在使用陈旧的数据,并意识到这一点。对于第二个参数,我们在 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中的 使用上一页()= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =将以前的值与当前值进行比较。 导入 是平等的UseRef。将以前的值与 useEffect ()中的当前值进行比较。如果他们是相同的做别的更新。使用上一个(值)是一个定制的钩子,用 UseRef ()创建一个 裁判

下面是我的代码片段。我面临的问题与无限循环更新数据使用防火库钩子

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()
}
     

})

如果您正在构建自定义钩子 ,有时可以使用缺省值导致无限循环,如下所示

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 = {}
}

如果在 < em > useEffect 的末尾包含空数组:

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

它会运行一次。

如果数组中也包含参数:

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

只要文本参数发生变化,它就会运行。

如果你确实需要比较对象,当它被更新时,这里有一个 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 和权限角色,并相应地更新应用程序的状态。

我的案例是遇到无限循环的特例,情况是这样的:

我有一个对象,比如来自道具的 objecX,我在道具中解构它,比如:

const { something: { somePropery } } = ObjX

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


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


它给我造成了一个无限循环,我试图通过传递整个 something作为一个依赖项来处理这个问题,它正常工作。

主要问题是 useEffect 将传入值与当前值 很浅进行比较。这意味着使用’= = =’比较来比较这两个值,它只检查对象引用,尽管数组和对象值是相同的,但它将它们视为两个不同的对象。我建议您查看我的 文章关于 useEffect 作为生命周期方法的文章。

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

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

当有一个复杂对象作为状态并从 useRef更新它时,我经常会遇到无限的重新呈现:

const [ingredients, setIngredients] = useState({});


useEffect(() => {
setIngredients({
...ingredients,
newIngedient: { ... }
});
}, [ingredients]);

在这种情况下,eslint(react-hooks/exhaustive-deps)强制我(正确地)将 ingredients添加到依赖项数组。但是,这会导致无限的重新呈现。与这个线程中的某些说法不同,这是正确的,并且不能将 ingredients.someKeyingredients.length放入依赖数组中。

解决方案是 setter 提供您可以引用的旧值。你应该这样做,而不是直接引用 ingredients:

const [ingredients, setIngredients] = useState({});


useEffect(() => {
setIngredients(oldIngedients => {
return {
...oldIngedients,
newIngedient: { ... }
}
});
}, []);