简体   繁体   English

useEffect 中的无限循环

[英]Infinite loop in useEffect

I've been playing around with the new hook system in React 16.7-alpha and get stuck in an infinite loop in useEffect when the state I'm handling is an object or array.当我正在处理的 state 是 object 或数组时,我一直在使用 React 16.7-alpha 中的新钩子系统并在 useEffect 中陷入无限循环。

First, I use useState and initiate it with an empty object like this:首先,我使用 useState 并使用一个空的 object 启动它,如下所示:

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

Then, in useEffect, I use setObj to set it to an empty object again.然后,在 useEffect 中,我再次使用 setObj 将其设置为空 object。 As a second argument I'm passing [obj], hoping that it wont update if the content of the object hasn't changed.作为第二个参数,我传递 [obj],希望如果 object 的内容没有改变,它不会更新。 But it keeps updating.但它一直在更新。 I guess because no matter the content, these are always different objects making React thinking it keep changing?我猜是因为无论内容如何,这些都是不同的对象,让 React 认为它在不断变化?

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

The same is true with arrays, but as a primitive it wont get stuck in a loop, as expected. arrays 也是如此,但作为一个原语,它不会像预期的那样陷入循环。

Using these new hooks, how should I handle objects and array when checking weather the content has changed or not?使用这些新的钩子,在检查天气内容是否发生变化时,我应该如何处理对象和数组?

Passing an empty array as the second argument to useEffect makes it only run on mount and unmount, thus stopping any infinite loops.将空数组作为第二个参数传递给 useEffect 使其仅在挂载和卸载时运行,从而停止任何无限循环。

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

This was clarified to me in the blog post on React hooks at https://www.robinwieruch.de/react-hooks/我在https://www.robinwieruch.de/react-hooks/上关于 React hooks 的博客文章中向我澄清了这一点

Had the same problem.有同样的问题。 I don't know why they not mention this in docs.我不知道他们为什么不在文档中提及这一点。 Just want to add a little to Tobias Haugen answer.只想对 Tobias Haugen 的回答补充一点。

To run in every component/parent rerender you need to use:要在每个组件/父级重新渲染中运行,您需要使用:

  useEffect(() => {

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

To run anything only one time after component mount(will be rendered once) you need to use:要在组件安装后运行一次(将呈现一次),您需要使用:

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

To run anything one time on component mount and on data/data2 change:在组件安装和 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] )

How i use it most of the time:我大部分时间是如何使用它的:

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

I ran into the same problem too once and I fixed it by making sure I pass primitive values in the second argument [] .我也遇到过同样的问题,我通过确保在第二个参数[]传递原始值来修复它。

If you pass an object, React will store only the reference to the object and run the effect when the reference changes, which is usually every singe time (I don't now how though).如果你传递一个对象,React 将只存储对对象的引用并在引用改变时运行效果,这通常是每一次(但我现在不知道如何)。

The solution is to pass the values in the object.解决方案是传递对象中的值。 You can try,你可以试试,

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

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

or或者

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

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

If you are building a custom hook , you can sometimes cause an infinite loop with default as follows如果您正在构建自定义钩子,有时会导致无限循环,默认情况如下

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

The fix is to use the same object instead of creating a new one on every function call:解决方法是使用相同的对象,而不是在每次函数调用时都创建一个新对象:

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

If you are encountering into this in your component code the loop may get fixed if you use defaultProps instead of ES6 default values如果您在组件代码中遇到此问题,如果您使用 defaultProps 而不是 ES6 默认值,循环可能会得到修复

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

MyComponent.defaultProps = {
  values = {}
}

As said in the documentation ( https://reactjs.org/docs/hooks-effect.html ), the useEffect hook is meant to be used when you want some code to be executed after every render .正如文档( https://reactjs.org/docs/hooks-effect.html )中所述,当您希望在每次渲染后执行某些代码时,应使用useEffect钩子。 From the docs:从文档:

Does useEffect run after every render? useEffect 是否在每次渲染后运行? Yes!是的!

If you want to customize this, you can follow the instructions that appear later in the same page ( https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects ).如果您想对此进行自定义,您可以按照同一页面稍后显示的说明进行操作 ( https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects )。 Basically, the useEffect method accepts a second argument , that React will examine to determine if the effect has to be triggered again or not.基本上, useEffect方法接受第二个参数,React 将检查该参数以确定是否必须再次触发效果。

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

You can pass any object as the second argument.您可以将任何对象作为第二个参数传递。 If this object remains unchanged, your effect will only be triggered after the first mount .如果这个对象保持不变,你的效果只会在第一次坐骑后触发 If the object changes, the effect will be triggered again.如果对象发生变化,将再次触发该效果。

If you include empty array at the end of useEffect :如果在useEffect末尾包含空数组:

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

It would run once.它会运行一次。

If you include also parameter on array:如果您还包括数组上的参数:

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

It would run whenever text parameter change.只要文本参数改变,它就会运行。

Your infinite loop is due to circularity你的无限循环是由于循环

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

setIngredients({}); will change the value of ingredients (will return a new reference each time), which will run setIngredients({}) .将改变ingredients的值(每次都会返回一个新的引用),这将运行setIngredients({}) To solve this you can use either approach:要解决此问题,您可以使用任一方法:

  1. Pass a different second argument to useEffect将不同的第二个参数传递给 useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
  setIngredients({});
}, [timeToChangeIngrediants ]);

setIngrediants will run when timeToChangeIngrediants has changed. setIngrediants将在timeToChangeIngrediants更改时运行。

  1. I'm not sure what use case justifies change ingrediants once it has been changed.我不确定一旦更改成分,什么用例证明更改成分是合理的。 But if it is the case, you pass Object.values(ingrediants) as a second argument to useEffect.但如果是这种情况,您可以将Object.values(ingrediants)作为第二个参数传递给 useEffect。
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));

I'm not sure if this will work for you but you could try adding .length like this:我不确定这是否适合你,但你可以尝试添加 .length 像这样:

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

In my case (I was fetching an array!) it fetched data on mount, then again only on change and it didn't go into a loop.在我的情况下(我正在获取一个数组!)它在挂载时获取数据,然后再次仅在更改时获取并且没有进入循环。

If you use this optimization, make sure the array includes all values from the component scope (such as props and state) that change over time and that are used by the effect.如果您使用此优化,请确保数组包含组件范围内的所有值(例如 props 和 state),这些值随时间变化并被效果使用。

I believe they are trying to express the possibility that one could be using stale data, and to be aware of this.我相信他们正试图表达人们可能使用陈旧数据的可能性,并意识到这一点。 It doesn't matter the type of values we send in the array for the second argument as long as we know that if any of those values change it will execute the effect.我们在array为第二个参数发送的值的类型并不重要,只要我们知道如果这些值中的任何一个发生变化,它将执行效果。 If we are using ingredients as part of the computation within the effect, we should include it in the 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]);

The best way is to compare previous value with current value by using usePrevious() and _.isEqual() from Lodash .最好的办法是通过使用usePrevious()Lodash _.isEqual()当前值进行比较之前的值。 Import isEqual and useRef .导入isEqualuseRef Compare your previous value with current value inside the useEffect() .将您之前的值与useEffect() 中的当前值进行比较。 If they are same do nothing else update.如果它们相同,则什么都不做更新。 usePrevious(value) is a custom hook which create a ref with useRef() . usePrevious(value)是一个自定义钩子,它使用useRef()创建一个ref

Below is snippet of my code.下面是我的代码片段。 I was facing problem of infinite loop with updating data using firebase hook我遇到了使用 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() } })

In case you DO need to compare the object and when it is updated here is a deepCompare hook for comparison.如果您确实需要比较对象,并且当它更新时,这里有一个deepCompare钩子用于比较。 The accepted answer surely does not address that.接受的答案肯定没有解决这个问题。 Having an [] array is suitable if you need the effect to run only once when mounted.如果您需要效果在安装时仅运行一次,则使用[]数组是合适的。

Also, other voted answers only address a check for primitive types by doing obj.value or something similar to first get to the level where it is not nested.此外,其他投票答案仅通过执行obj.value或类似的操作来检查原始类型,以首先到达未嵌套的级别。 This may not be the best case for deeply nested objects.对于深度嵌套的对象,这可能不是最好的情况。

So here is one that will work in all cases.所以这是一种适用于所有情况的方法。

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;
};

You can use the same in useEffect hook您可以在useEffect钩子中使用相同的

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

You could also destructure the object in the dependency array, meaning the state would only update when certain parts of the object updated.您还可以解构依赖数组中的对象,这意味着状态只会在对象的某些部分更新时更新。

For the sake of this example, let's say the ingredients contained carrots, we could pass that to the dependency, and only if carrots changed, would the state update.就本例而言,假设成分包含胡萝卜,我们可以将其传递给依赖项,并且只有当胡萝卜发生变化时,状态才会更新。

You could then take this further and only update the number of carrots at certain points, thus controlling when the state would update and avoiding an infinite loop.然后你可以更进一步,只在某些点更新胡萝卜的数量,从而控制状态何时更新并避免无限循环。

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

An example of when something like this could be used is when a user logs into a website.何时可以使用此类内容的一个示例是用户登录网站时。 When they log in, we could destructure the user object to extract their cookie and permission role, and update the state of the app accordingly.当他们登录时,我们可以解构用户对象以提取他们的 cookie 和权限角色,并相应地更新应用程序的状态。

The main problem is that useEffect compares the incoming value with the current value shallowly .主要问题是 useEffect 对传入值与当前值进行了较浅的比较 This means that these two values compared using '===' comparison which only checks for object references and although array and object values are the same it treats them to be two different objects.这意味着使用“===”比较来比较这两个值,该比较仅检查对象引用,尽管数组和对象值相同,但将它们视为两个不同的对象。 I recommend you to check out my article about useEffect as a lifecycle methods.我建议你看看我的文章有关useEffect的生命周期方法。

my Case was special on encountering an infinite loop, the senario was like this:我的案例在遇到无限循环时很特别,场景是这样的:

I had an Object, lets say objX that comes from props and i was destructuring it in props like:我有一个对象,假设 objX 来自道具,我正在道具中解构它,例如:

const { something: { somePropery } } = ObjX

and i used the somePropery as a dependency to my useEffect like:我使用somePropery作为对useEffect的依赖,例如:


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

and it caused me an infinite loop, i tried to handle this by passing the whole something as a dependency and it worked properly.它导致我陷入无限循环,我试图通过将整个something作为依赖项传递来处理这个问题,并且它正常工作。

I often run into an infinite re-render when having a complex object as state and updating it from useRef :当复杂的 object 为 state 并从useRef更新时,我经常遇到无限重新渲染:

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

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

In this case eslint(react-hooks/exhaustive-deps) forces me (correctly) to add ingredients to the dependency array.在这种情况下eslint(react-hooks/exhaustive-deps)迫使我(正确地)将ingredients添加到依赖数组中。 However, this results in an infinite re-render.但是,这会导致无限的重新渲染。 Unlike what some say in this thread, this is correct, and you can't get away with putting ingredients.someKey or ingredients.length into the dependency array.与此线程中某些人所说的不同,这是正确的,并且您无法摆脱将ingredients.someKeyingredients.length .长度放入依赖项数组中。

The solution is that setters provide the old value that you can refer to.解决方案是 setter 提供您可以参考的旧值。 You should use this, rather than referring to ingredients directly:您应该使用它,而不是直接引用ingredients

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

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

Another worked solution that I used for arrays state is:我用于数组状态的另一个有效解决方案是:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM