166

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

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

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

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

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

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

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

4

16 回答 16

179

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

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

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

于 2018-10-30T23:53:31.940 回答
108

有同样的问题。我不知道他们为什么不在文档中提到这一点。只是想在 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>  
}
于 2018-10-31T20:08:45.040 回答
26

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

如果您传递一个对象,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]);
于 2019-02-09T04:32:18.390 回答
23

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

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 = {}
}
于 2019-10-29T06:41:11.863 回答
13

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

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

于 2019-02-11T18:02:28.993 回答
11

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

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

它会运行一次。

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

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

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

于 2020-03-02T09:08:38.940 回答
11

你的无限循环是由于循环

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));
于 2019-06-29T14:08:17.833 回答
9

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

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

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

于 2019-04-27T16:09:48.630 回答
3

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

于 2019-08-22T15:53:57.020 回答
3

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

于 2021-03-23T10:19:09.510 回答
2

如果您确实需要比较对象并且当它被更新时,这里是一个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]));
于 2020-04-19T13:21:05.320 回答
2

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

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

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

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

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

于 2020-07-24T10:17:48.137 回答
2

最好的方法是使用 Lodash 中的 usePrevious() 和 _.isEqual() 将之前的值当前进行比较。导入isEqualuseRef。将您之前的值与useEffect()中的当前值进行比较。如果它们相同,则不执行任何更新。usePrevious(value)是一个自定义钩子,它使用useRef()创建一个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()
      }
     
  })

于 2019-09-06T13:58:09.500 回答
1

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

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

const { something: { somePropery } } = ObjX

我把它用作我喜欢somePropery的依赖项:useEffect


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

它给我造成了一个无限循环,我试图通过将整体something作为依赖项传递来处理这个问题,它工作正常。

于 2020-09-27T10:01:37.560 回答
0

当将复杂对象作为状态并从以下位置更新时,我经常遇到无限重新渲染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: { ... }
    }
  });
}, []);
于 2022-02-07T08:17:37.433 回答
0

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

useEffect(() => {
  setIngredients(ingredients.length ? ingredients : null);
}, [ingredients]);
于 2021-05-11T23:13:18.917 回答