useEffect 中的无限循环

Infinite loop in useEffect

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

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

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

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

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

数组也是如此,但作为原始元素,它不会像预期的那样陷入循环。

使用这些新的钩子,在检查内容是否发生变化时我应该如何处理对象和数组?

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

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

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

的 React hooks 博客 post 向我阐明了这一点

有同样的问题。我不知道他们为什么不在文档中提及这一点。只想对 Tobias Haugen 的回答做一点补充。

中运行每component/parent重新渲染你需要使用:

  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 钩子用于 当您希望在每次渲染后执行某些代码时 .来自文档:

Does useEffect run after every render? Yes!

如果您想要对此进行自定义,您可以按照同一页面稍后出现的说明进行操作 (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

您可以将任何对象作为第二个参数传递。 如果这个对象保持不变,你的效果只会在第一次挂载后触发。如果对象发生变化,效果将再次触发。

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

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

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

你的死循环是因为循环

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

setIngredients({}); 将更改 ingredients 的值(每次都会 return 一个新引用),这将 运行 setIngredients({})。要解决此问题,您可以使用以下任一方法:

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

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

  1. 我不确定什么用例证明更改成分后更改成分是合理的。但如果是这样,您将 Object.values(ingrediants) 作为第二个参数传递给 useEffect。
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));

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.

我相信他们正试图表达人们可能正在使用陈旧数据并意识到这一点的可能性。我们在 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]);

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

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

如果你正在构建一个自定义的钩子,你有时会导致一个默认的无限循环,如下所示

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

如果在 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 和权限角色,并相应地更新应用程序的状态。

我的Case遇到死循环比较特殊,senario是这样的:

我有一个对象,可以说是来自 props 的 objX,我在像这样的 props 中解构它:

const { something: { somePropery } } = ObjX

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


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

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

主要问题是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关于useEffect作为生命周期方法。

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

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

当有一个复杂的对象作为状态并从 useRef 更新它时,我经常 运行 进入无限 re-render:

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

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

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

解决方法是设置器提供您可以引用的旧值。你应该使用这个,而不是直接引用 ingredients

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

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