Бесконечный цикл в использованииEffect
Я поигрался с новой системой ловушек в React 16.7-alpha и застрял в бесконечном цикле в useEffect, когда обрабатываемое мной состояние является объектом или массивом.
Сначала я использую useState и запускаю его с пустым объектом, например так:
const [obj, setObj] = useState({});
Затем в useEffect я использую setObj, чтобы снова установить его в пустой объект. В качестве второго аргумента я передаю [obj], надеясь, что он не обновится, если содержимое объекта не изменилось. Но он продолжает обновляться. Я думаю, потому что независимо от содержания, это всегда разные объекты, заставляющие React думать, что оно постоянно меняется?
useEffect(() => {
setIngredients({});
}, [ingredients]);
То же самое верно и для массивов, но, как примитив, он не застрянет в цикле, как ожидалось.
Используя эти новые хуки, как мне обращаться с объектами и массивом при проверке того, изменился контент или нет?
Ответы
Ответ 1
Передача пустого массива в качестве второго аргумента для useEffect заставляет его работать только при монтировании и размонтировании, тем самым останавливая любые бесконечные циклы.
useEffect(() => {
setIngredients({});
}, []);
Это было разъяснено мне в сообщении в блоге о перехватах React на https://www.robinwieruch.de/react-hooks/
Ответ 2
Была такая же проблема. Я не знаю, почему они не упоминают об этом в документах. Просто хочу немного добавить к ответу Тобиаса Хаугена.
Для запуска каждого компонента/родительского повторного рендеринга вам необходимо использовать:
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 does not 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 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>
}
Ответ 3
Как сказано в документации (https://reactjs.org/docs/hooks-effect.html), useEffect
предназначена для использования, когда вы хотите, чтобы какой-то код выполнялся после каждого рендеринга. Из документов:
Запускается ли useEffect после каждого рендера? Да!
Если вы хотите настроить это, вы можете следовать инструкциям, которые появятся позже на той же странице (https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects). По useEffect
метод useEffect
принимает второй аргумент, который React проверит, чтобы определить, нужно ли снова запускать эффект или нет.
useEffect(() => {
document.title = 'You clicked ${count} times';
}, [count]); // Only re-run the effect if count changes
Вы можете передать любой объект в качестве второго аргумента. Если этот объект остается неизменным, ваш эффект будет срабатывать только после первого монтирования. Если объект изменится, эффект будет запущен снова.
Ответ 4
Я тоже однажды столкнулся с той же проблемой и исправил ее, убедившись, что передал примитивные значения во втором аргументе []
.
Если вы передадите объект, 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
Я не уверен, что это сработает для вас, но вы можете попробовать добавить .length следующим образом:
useEffect(() => {
// fetch from server and set as obj
}, [obj.length]);
В моем случае (я выбирал массив!) Он получал данные при монтировании, затем снова только при изменении и не зацикливался.
Ответ 6
Если вы используете эту оптимизацию, убедитесь, что массив включает в себя все значения из области компонента (например, реквизиты и состояние), которые меняются со временем и используются эффектом.
Я полагаю, что они пытаются выразить возможность того, что кто-то может использовать устаревшие данные, и знать об этом. Не имеет значения тип значений, которые мы отправляем в 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]);
Ответ 7
Ваш бесконечный цикл из-за округлости
useEffect(() => {
setIngredients({});
}, [ingredients]);
setIngredients({});
изменит значение ingredients
(каждый раз будет возвращать новую ссылку), что запустит setIngredients({})
. Для решения этой проблемы вы можете использовать любой подход:
- Передайте другой второй аргумент для useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
setIngredients({});
}, [timeToChangeIngrediants ]);
setIngrediants
будет работать, когда timeToChangeIngrediants
изменился.
- Я не уверен, какой вариант использования оправдывает изменение ингредиентов после его изменения. Но если это так, вы передаете
Object.values(ingrediants)
в качестве второго аргумента для использованияEffect.
useEffect(() => {
setIngredients({});
}, Object.values(ingrediants));
Ответ 8
Если вы устанавливаете новое состояние в хуке UseEffect, и в то же время в DOM в зависимости от этого состояния есть побочные эффекты, это создаст бесконечный цикл.
function DisplayCounter(props) {
const [result, setResult] = useState(0)
useEffect(() => {
setResult(result + 1)
console.log(result)
});
return (
<div>
{result}
</div>
)
}
Поэтому один из способов избежать этого - использовать приведенные выше предложения (передавая второй аргумент с пустым массивом). Другой способ - избежать изменения состояния внутри хука useEffect.
Ответ 9
У меня проблемы с бесконечным циклом, я создаю коды andbox.
https://codesandbox.io/s/suspicious-bassi-tedfo
Как я могу избежать этого?
Ответ 10
Лучший способ - сравнить предыдущее значение с текущим, используя usePrevious() и _.isEqual() из Lodash.
Импортируйте isEqual и useRef. Сравните ваше предыдущее значение с текущим значением внутри useEffect(). Если они одинаковые, ничего не обновляйте. usePrevious (value) - это пользовательский хук, который создает ref с помощью useRef().
Ниже приведен фрагмент моего кода. Я столкнулся с проблемой бесконечного цикла с обновлением данных с помощью 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()
}
})