使用中的无限循环


115

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

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

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

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

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

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

使用这些新的挂钩,在检查天气内容是否已更改时,应该如何处理对象和数组?


Tobias,什么用例需要在其值更改后更改ingrediants的值?
本·卡普

@Tobias,您应该阅读我的回答。我相信您会接受它作为正确的答案。
HalfWebDev

Answers:


128

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

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

我在https://www.robinwieruch.de/react-hooks/上关于React hooks的博客文章中向我阐明了这一点。


1
它实际上是一个空数组,而不是您应该传递的空对象
。– GifCo

20
这不能解决问题,您需要传递该钩子使用的依赖项
helado

1
请注意,在卸载时,如果已指定效果,则效果将运行清除功能。实际效果不会在卸载时运行。 reactjs.org/docs/hooks-effect.html#example-using-hooks-1
托尼

2
正如Dan Abramov所说,当setIngrediets是一个依赖项时使用空数组是一种反模式。不要将useEffetct像componentDidMount()方法一样对待
p7adams

1
因为上面的人没有提供关于如何正确解决这个问题的链接或资源,我建议迎面而来duckduckgoers检查了这一点,因为它固定我的无限循环的问题:stackoverflow.com/questions/56657907/...
C.肋骨

78

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

  useEffect(() => {
    loadBookFromServer()
  }, [id]) // every time id changed, new book will be loaded

  // Remeber that it's not always safe to omit functions from the list of dependencies. Explained in comments.
  async function loadBookFromServer() {
    let response = await fetch('api/book/' + id)
    response  = await response.json() 
    bookSet(response)
  }

  if (!book) return false //first render, when useEffect did't triggered yet we will return false

  return <div>{JSON.stringify(book)}</div>  
}

2
根据React常见问题解答,从依赖项列表中省略函数是不安全的
egdavid

@endavid取决于您使用的道具是什么
ZiiMakc

1
当然,如果您不使用组件范围内的任何值,则可以忽略。但是从纯粹的设计/架构角度来看,这不是一个好习惯,因为如果您需要使用道具,它需要您将整个功能移到效果内部,并且最终可能会使用useEffect方法,该方法将使用大量的代码行。
egdavid

18

我也遇到了同样的问题,并通过确保在第二个参数中传递原始值来解决该问题[]

如果您传递一个对象,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]);

4
另一种方法是使用useMemo创建此类值,这样就保留了引用,并且对依赖项的值进行了评估
helado

@helado,您可以将useMemo()用于值或将useCallback()用于函数
Juanma Menendez,

该随机值是否传递给依赖项数组?当然,这可以防止无限循环,但这是否受到鼓励?我也可以传递0给依赖项数组吗?
thinkvantagedu

11

如文档(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

您可以将任何对象作为第二个参数传递。如果此对象保持不变,则只会在第一次安装后触发效果。如果对象发生更改,效果将再次被触发。


11

如果要构建自定义钩子,有时会导致无限循环,默认情况如下:

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

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

1
你救了我的一天!多谢兄弟。
Han Van Pham

7

我不确定这是否适合您,但您可以尝试添加.length,如下所示:

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

就我而言(我正在获取一个数组!),它在安装时获取数据,然后仅在更改时获取数据,并且没有进入循环。


1
如果替换数组中的项目怎么办?在这种情况下,数组的长度将是相同的,并且效果将无法实现!
阿米尔汗

7

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

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

它会运行一次。

如果在数组上还包含参数:

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

每当文本参数更改时,它将运行。


为什么在钩子末尾放置一个空数组,它只会运行一次?
卡伦

useEffect末尾的空数组是开发人员有目的的实现,用于在例如可能需要在useEffect内设置setState的情况下停止无限循环。否则,这将导致useEffect->状态更新-> useEffect->无限循环。
to240

空数组会引起eslinter警告。
hmcclungiii

4

您的无限循环是由于圆度

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

2

如果使用此优化,请确保数组包含组件范围中所有随时间变化并被效果使用的值(例如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]);


1

最好的办法是通过使用当前值进行比较前值usePrevious()_.isEqual()Lodash。导入isEqualuseRef。将您先前的值与useEffect()中的当前值进行比较。如果它们相同,则不进行其他任何更新。usePrevious(value)是一个自定义钩子,可使用useRef()创建引用

以下是我的代码段。我正面临使用Firebase钩子更新数据的无限循环问题

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


1
我想人们对此表示反对,因为您建议使用第三方库。但是,基本思想是好的。
jperl

1

万一您需要比较对象,并且在更新对象时,这里有一个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]));

0

您还可以在依赖关系数组中分解对象,这意味着状态仅在对象的某些部分更新时才会更新。

就本例而言,假设成分中包含胡萝卜,我们可以将其传递给依赖项,并且只有在胡萝卜发生变化时,状态才会更新。

然后,您可以进一步进行此操作,仅在某些点更新胡萝卜的数量,从而控制状态何时更新并避免无限循环。

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

当用户登录网站时,可以使用此类示例。当他们登录时,我们可以分解用户对象以提取其cookie和权限角色,并相应地更新应用程序的状态。


0

我的Case特别适合遇到无限循环,感觉就像这样:

我有一个对象,可以说来自道具的objX,我正在像这样的道具中对其进行销毁:

const { something: { somePropery } } = ObjX

并且我将somePropery用作我的依赖项useEffect


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

它导致了一个无限循环,我试图通过将整个something作为依赖项进行传递来处理此问题,并且它工作正常。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.