使用中的无限循环

时间:2018-10-30 18:45:54

标签: reactjs react-hooks

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

首先,我使用useState并使用一个空对象将其初始化:

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

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

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

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

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

15 个答案:

答案 0 :(得分:22)

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

with open('export_log.csv', 'w') as fd:
  output = csv.writer(fd)
  …

我在https://www.robinwieruch.de/react-hooks/

上有关React钩子的博客文章中向我阐明了这一点。

答案 1 :(得分:20)

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

  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 :(得分:6)

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

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

它将运行一次。

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

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

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

答案 3 :(得分:4)

如文档(https://reactjs.org/docs/hooks-effect.html中所述),{strong}当您希望在每次渲染后执行一些代码时使用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

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

答案 4 :(得分:3)

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

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

答案 5 :(得分: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]);

答案 6 :(得分:2)

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

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

答案 7 :(得分:2)

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

答案 8 :(得分: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]));

答案 9 :(得分:0)

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

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

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

答案 10 :(得分:0)

您的无限循环是由于圆度

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

答案 11 :(得分:0)

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

下面是我的代码段。我遇到了使用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()
      }
     
  })

答案 12 :(得分:0)

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

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

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

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

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

答案 13 :(得分:0)

我的Case在遇到无限循环时很特别,感觉就像这样:

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

const { something: { somePropery } } = ObjX

并且我使用somePropery作为对useEffect的依赖项,例如:


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

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

答案 14 :(得分:0)

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

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