ChatGPT解决这个技术问题 Extra ChatGPT

useEffect 中的无限循环

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

首先,我使用 useState 并使用这样的空对象启动它:

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

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

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

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

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

Tobias,一旦它的价值改变了,什么用例需要改变成分的价值?
@Tobias,您应该阅读我的答案。我相信你会接受它作为正确的答案。
我读了这篇article,它帮助我更清楚地理解了一些东西。我要做的是查找有关对象/数组的特定属性,例如元素的数量或名称(无论您喜欢什么),并将它们用作 useEffect 钩子中的依赖项

R
Retsam

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

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

我在 https://www.robinwieruch.de/react-hooks/ 上关于 React 钩子的博文中澄清了这一点


它实际上是一个空数组,而不是您应该传入的空对象。
这并不能解决问题,您需要传递钩子使用的依赖项
请注意,在卸载时,如果您指定了清理功能,效果将运行清理功能。实际效果不会在卸载时运行。 reactjs.org/docs/hooks-effect.html#example-using-hooks-1
正如 Dan Abramov 所说,当 setIngrediets 是依赖项时使用空数组是一种反模式。不要将 useEffectct 视为 componentDidMount() 方法
由于上面的人没有提供有关如何正确解决此问题的链接或资源,我建议迎面而来的鸭子爱好者检查一下,因为它解决了我的无限循环问题:stackoverflow.com/questions/56657907/…
Z
ZiiMakc

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

要在组件挂载和 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>  
}

@endavid 取决于您使用的道具是什么
当然,如果您不使用组件范围内的任何值,那么省略是安全的。但是从 vue 的纯粹设计/架构角度来看,这不是一个好习惯,因为如果您需要使用道具,它需要您将整个函数移动到效果内,并且您最终可能会得到一个将使用的 useEffect 方法数量惊人的代码行。
这真的很有帮助,调用 useEffect 挂钩中的异步函数仅在值更改时更新。谢了哥们
哇,我认为不传递任何参数与传递一个空数组是一样的……真令人头疼。
D
Dinesh Pandiyan

我也遇到过同样的问题,我通过确保在第二个参数 [] 中传递原始值来修复它。

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

另一种方法是使用 useMemo 创建这样的值,这样可以保留引用并且依赖项的值被评估为相同
@helado 您可以将 useMemo() 用于值或 useCallback() 用于函数
是否将随机值传递给依赖数组?当然,这可以防止无限循环,但这是鼓励吗?我也可以将 0 传递给依赖数组吗?
@Dinesh 如果您添加扩展运算符 }, ...[Object.values(obj)]); ... 或简单地删除方括号 }, Object.values(obj)); ...,第一个解决方案将起作用
J
Jkarttunen

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

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

谢谢!这对我来说并不明显,而正是我遇到的问题。
R
Rocío García Luque

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

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


B
Ben Carp

你的无限循环是由于循环

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

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

将不同的第二个参数传递给 useEffect

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

setIngrediants 将在 timeToChangeIngrediants 更改时运行。

我不确定一旦更改成分,哪些用例证明更改成分是合理的。但如果是这种情况,您将 Object.values(ingrediants) 作为第二个参数传递给 useEffect。

useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));

b
besartm

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

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

它会运行一次。

如果您还包括数组上的参数:

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

它会在文本参数更改时运行。


如果我们在钩子的末尾放置一个空数组,为什么它只会运行一次?
useEffect 末尾的空数组是开发人员有目的的实现,用于在您可能需要在 useEffect 内部设置状态的情况下停止无限循环。否则会导致 useEffect -> state update -> useEffect -> 无限循环。
空数组会导致 eslinter 发出警告。
以下是对此的详细说明:reactjs.org/docs/…
L
Luka M

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

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

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


如果数组中的某个项目被替换了怎么办?在这种情况下,数组的长度将是相同的,并且效果不会运行!
l
locomotif

如果您使用此优化,请确保数组包含组件范围内的所有值(例如 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]);


A
Ali Toshmatov

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


H
H S Progr

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

下面是我的代码片段。我在使用 firebase hook 更新数据时遇到了无限循环的问题

导入 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' 导出函数 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() } })


我猜人们对此表示反对,因为您建议使用第三方库。但是,基本的想法是好的。
这不是仍然会创建一个无限循环(尽管主体几乎是空的),因此浏览器会占用大量 CPU 周期吗?
@AlwaysLearning 你有更好的解决方案吗?
H
HalfWebDev

如果您确实需要比较对象并且当它被更新时,这里是一个 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]));

isEqual 函数从何而来?
@乔什鲍登 lodash
t
to240

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

为了这个例子,假设成分包含胡萝卜,我们可以将它传递给依赖项,并且只有当胡萝卜改变时,状态才会更新。

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

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

当用户登录网站时,可以使用这样的东西的一个例子。当他们登录时,我们可以解构用户对象以提取他们的 cookie 和权限角色,并相应地更新应用程序的状态。


a
amdev

我的案例在遇到无限循环时很特别,场景是这样的:

我有一个对象,假设 objX 来自道具,我在道具中解构它,例如:

const { something: { somePropery } } = ObjX

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


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

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


t
thisismydesign

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

A
AuthorProxy

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

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