useEffect中的无限循环

197

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

首先,我使用useState并将其初始化为空对象,就像这样:

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

然后,在 useEffect 中,我使用 setObj 将其设置为空对象。作为第二个参数,我传递 [obj],希望如果对象的内容没有更改它就不会更新。但它一直在更新。我想这是因为无论内容如何,这些始终是不同的对象,让 React 认为它一直在更改?

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

数组也是如此,但作为一个原始类型,它不会像预期的那样陷入循环中。

使用这些新钩子,当检查内容是否已更改时,我应该如何处理对象和数组?


1
Tobias,什么使用情况需要在成分的值发生变化后更改其值? - Ben Carp
1
@Tobias,你应该阅读我的答案。我相信你会接受它作为正确的答案。 - HalfWebDev
1
我读了这篇文章,它帮助我更清楚地理解了一些东西。我的做法是查找有关对象/数组的特定属性,例如元素数量或名称(任何您喜欢的内容),并将其用作useEffect钩子中的依赖项。 - emalinga
16个回答

216

将一个空数组作为第二个参数传递给 useEffect,使其仅在挂载和卸载时运行,从而停止任何无限循环。

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

https://www.robinwieruch.de/react-hooks/的React hooks博客文章中,这个问题对我有了更明确的解答。


2
实际上是一个空数组而不是一个空对象,你应该传入它。 - GifCo
84
这并不能解决问题,你需要传递被钩子使用的依赖项。 - helado
5
请注意,如果您指定了清理函数,当卸载组件时会运行 effect 的清理函数。实际的 effect 不会在组件卸载时运行。 - tony
7
正如Dan Abramov所说,当setIngrediets是依赖项时使用空数组是一种反模式。不要将useEffect视为componentDidMount()方法。 - p7adams
2
由于上面的人没有提供正确解决此问题的链接或资源,我建议即将使用DuckDuckGo的人们查看此链接,因为它解决了我的无限循环问题: https://dev59.com/vbTma4cB1Zd3GeqP8pTJ - C. Rib
显示剩余3条评论

131

我有同样的问题。不知道为什么他们在文档中没有提到这一点。只想在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
  }, [] )

为了在组件挂载和数据/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>  
}

8
根据React官方文档中的FAQ,从依赖项列表中省略函数是不安全的。 - Mooncake
1
@endavid 取决于您使用的属性。 - ZiiMakc
2
当然,如果您不使用组件作用域中的任何值,则可以安全地省略它。但从纯设计/架构角度来看,这并不是一个好的实践,因为如果需要使用props,则需要将整个函数移动到effect内部,这可能会导致useEffect方法使用过多的代码行。 - Mooncake
2
这真的很有帮助,在 useEffect 钩子中调用异步函数只有在值改变时才会更新。谢谢伙计。 - EMMANUEL OKELLO
@egdavid那么当一个值更新时,运行某个代码块的正确方法是什么?使用UseEffect会导致lint错误将该值包含在依赖项中,但这会导致无限循环,因此不能使用该方法。 - Embedded_Mugs
1
哇,我假设不传参数与传递一个空数组相同……真是一场头痛。 - Alex Hope O'Connor

34
我也曾遇到过同样的问题,解决方法是确保将原始值作为第二个参数传递[]
如果传递一个对象,React将只存储对该对象的引用,并在引用发生更改时运行effect,这通常是每次都会发生的(我不知道具体原因)。
解决方法是将值作为对象传递。你可以尝试一下,
const obj = { keyA: 'a', keyB: 'b' }

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

7
另一种方法是使用useMemo创建这些值,这样引用将保留,并且依赖项的值将被评估为相同的值。 - helado
2
@helado 你可以使用 useMemo() 来处理值,或者使用 useCallback() 来处理函数。 - Juanma Menendez
1
这是随机值传递到依赖数组吗?当然,这可以防止无限循环,但这是被鼓励的吗?我也可以将0传递到依赖数组中吗? - one-hand-octopus
1
@Dinesh 如果你添加了扩展运算符}, ...[Object.values(obj)]); ...,第一个解决方案将会起作用,或者你可以简单地删除方括号}, Object.values(obj)); ... - vtolentino

27

如果你正在构建一个自定义 hook,有时候默认值可能会导致无限循环,如下所示

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

3
谢谢!对我来说这不是显而易见的问题,正是我遇到的问题。 - stuckj

14

你的无限循环是由于循环性引起的

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

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

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

setIngrediants会在timeToChangeIngrediants发生变化时运行。

  1. 我不确定有什么用例可以证明在更改后再次更改成分是有意义的。但如果确实需要这样做,您可以将Object.values(ingrediants)作为第二个参数传递给useEffect。
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));

13

我不确定这对你是否有效,但你可以尝试像这样添加 .length:

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

在我的情况下(我正在获取一个数组!),它在挂载时获取数据,然后只会在更改时再次获取数据,而不会进入循环。


4
如果在数组中替换了一个项,那么数组的长度将保持不变,效果也不会生效! - Aamir Khan

13

如文档所述 (https://reactjs.org/docs/hooks-effect.html),useEffect hook 用于在每次渲染后执行一些代码。根据文档:

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
您可以将任何对象作为第二个参数传递。如果此对象保持不变,则您的效果仅在第一次挂载后触发。如果对象发生更改,则会再次触发该效果。

您可以将任何对象作为第二个参数传递。如果该对象保持不变,您的 effect 只会在首次挂载后触发。如果该对象发生更改,则 effect 会再次触发。


11

如果你在 useEffect 的最后包含一个空数组:

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

它只会运行一次。

如果你也在数组上包含参数:

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

只要文本参数更改,它就会运行。


1
如果我们在钩子的末尾放置一个空数组,为什么它只会运行一次? - Karen
1
在 useEffect 的结尾处使用空数组是开发人员有意为之的实现,目的是防止出现无限循环的情况。例如,在 useEffect 中需要使用 setState 时,否则会导致 useEffect -> 状态更新 -> useEffect -> 无限循环。 - to240
2
一个空数组会导致 esLint 发出警告。 - hmcclungiii
1
这里有一个详细的解释:https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects - Franchy

7
当状态为复杂对象并从 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: { ... }
    }
  });
}, []);

这是一个真正的答案,对于我们中的那些处于需要修改状态而无法将其列为依赖项或者会出现无限循环的情况。 - Malvineous

3
如果您使用此优化,请确保数组包括组件作用域中(例如道具和状态)随时间变化并且被effect使用的所有值。
我认为他们试图表达的是可能会使用过时的数据,需要注意这一点。只要我们知道如果这些值中的任何一个发生了变化,就会执行该effect,那么我们发送到第二个参数 array 中的值类型都无关紧要。如果我们在effect内部使用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]);


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接