Я поигрался с новой системой ловушек в React 16.7-alpha и застрял в бесконечном цикле в useEffect, когда состояние, которое я обрабатываю, является объектом или массивом.
Сначала я использую useState и запускаю его с пустым объектом, например:
const [obj, setObj] = useState({});
Затем в useEffect я использую setObj, чтобы снова установить для него пустой объект. В качестве второго аргумента я передаю [obj], надеясь, что он не обновится, если содержание объекта не изменилось. Но он продолжает обновляться. Я думаю, потому что независимо от содержимого, это всегда разные объекты, заставляющие React думать, что он постоянно меняется?
useEffect(() => {
setIngredients({});
}, [ingredients]);
То же самое и с массивами, но как примитив он не застревает в цикле, как ожидалось.
Используя эти новые хуки, как мне обрабатывать объекты и массив при проверке того, изменилось ли содержимое или нет?
@Tobias, прочти мой ответ. Я уверен, что вы примете это как правильный ответ.
Я прочитал этот статья, и он помог мне кое-что понять более ясно. Что бы я сделал, так это поиск определенных атрибутов объекта / массива, таких как количество элементов или имя (как вам угодно), и использовал бы их как зависимости в хуке useEffect.





Передача пустого массива в качестве второго аргумента для useEffect заставляет его работать только при монтировании и размонтировании, тем самым останавливая любые бесконечные циклы.
useEffect(() => {
setIngredients({});
}, []);
Это было разъяснено мне в сообщении блога о перехватчиках React на https://www.robinwieruch.de/react-hooks/
На самом деле это пустой массив, а не пустой объект, который вы должны передать.
Это не решает проблему, вам нужно передать зависимости, используемые хуком.
Обратите внимание, что при отключении эффект запустит функцию очистки, если вы ее указали. Фактический эффект не работает при размонтировании. reactjs.org/docs/hooks-effect.html#example-using-hooks-1
Как сказал Дэн Абрамов, использование пустого массива, когда setIngrediets является зависимостью, является антипаттерном. Не относитесь к useEffetct как к методу componentDidMount ()
НЕ ИСПОЛЬЗУЙТЕ ЕГО. Вы получите исчерпывающую ошибку deps: github.com/facebook/react/issues/14920
поскольку приведенные выше люди не предоставили ссылку или ресурс о том, как правильно решить эту проблему, я предлагаю встречным любителям уток проверить это, поскольку это решило мою проблему с бесконечным циклом: stackoverflow.com/questions/56657907/…
Можете ли вы помочь мне с подобной проблемой? stackoverflow.com/questions/67773860/…
Была такая же проблема. Я не знаю, почему они не упоминают об этом в документации. Просто хочу добавить немного к ответу Тобиаса Хогена.
Для запуска в каждый компонент / родительский рендер вам необходимо использовать:
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, небезопасно исключать функции из списка зависимостей.
@endavid зависит от того, какую опору вы используете
конечно, если вы не используете значения любой из области действия компонента, то можно безопасно опустить. Но с чисто точки зрения проектирования / архитектуры vue это не очень хорошая практика, поскольку она требует, чтобы вы переместили всю функцию внутри эффекта, если вам нужно использовать props, и вы можете получить метод useEffect, который будет использовать невероятное количество строк кода.
Это действительно полезно, поскольку вызов асинхронной функции внутри хука useEffect обновляется только при изменении значения. Спасибо друг
@egdavid, тогда как правильно запускать определенный блок кода при обновлении значения? Использование UseEffect вызывает ошибку lint для включения значения в зависимость, но это вызывает бесконечный цикл, поэтому этот метод нельзя использовать
Я тоже однажды столкнулся с той же проблемой и исправил ее, убедившись, что я передаю примитивные значения во втором аргументе [].
Если вы передадите объект, 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)); ...
Как сказано в документации (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 (каждый раз будет возвращать новую ссылку), что запустит setIngredients({}). Чтобы решить эту проблему, вы можете использовать любой подход:
const timeToChangeIngrediants = .....
useEffect(() => {
setIngredients({});
}, [timeToChangeIngrediants ]);
setIngrediants будет работать после изменения timeToChangeIngrediants.
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 () и _.равно() из Лодаш. Импортируйте равно и useRef. Сравните ваше предыдущее значение с текущим значением внутри useEffect (). Если они такие же, больше ничего не обновляйте. usePrevious (значение) - это настраиваемая ловушка, которая создает ссылка с useRef ().
Ниже приведен фрагмент моего кода. Я столкнулся с проблемой бесконечного цикла с обновлением данных с помощью крючка 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()
}
})Я предполагаю, что люди проголосовали против этого, потому что вы предложили использовать стороннюю библиотеку. Однако основная идея хороша.
Если вы создаете собственный хук, иногда вы можете вызвать бесконечный цикл по умолчанию следующим образом
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])
Он будет запускаться при изменении текстового параметра.
почему он будет запускаться только один раз, если мы поместим пустой массив в конец хука?
Пустой массив в конце useEffect - это целенаправленная реализация разработчиками для остановки бесконечных циклов в ситуациях, когда вам, например, может потребоваться setState внутри useEffect. В противном случае это привело бы к useEffect -> обновление состояния -> useEffect -> бесконечный цикл.
Пустой массив вызывает предупреждение от eslinter.
Вот подробное объяснение этого: reactjs.org/docs/…
Если вам НЕОБХОДИМО сравнивать объект и когда он обновляется, вот ловушка 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?
Вы также можете деструктурировать объект в массиве зависимостей, то есть состояние будет обновляться только при обновлении определенных частей объекта.
Для этого примера предположим, что ингредиенты содержат морковь, мы могли бы передать это в зависимость, и только если морковь изменилась, состояние обновилось.
Затем вы можете пойти дальше и обновлять количество морковок только в определенных точках, тем самым контролируя, когда будет обновляться состояние, и избегая бесконечного цикла.
useEffect(() => {
setIngredients({});
}, [ingredients.carrots]);
Примером использования чего-то подобного является вход пользователя на веб-сайт. Когда они входят в систему, мы можем деструктурировать объект пользователя, чтобы извлечь их cookie и роль разрешений, и соответствующим образом обновить состояние приложения.
мой случай был особенным при столкновении с бесконечным циклом, сенарио было таким:
У меня был объект, скажем, objX, который исходит из реквизита, и я деструктурировал его в реквизитах, например:
const { something: { somePropery } } = ObjX
и я использовал somePropery как зависимость от моего useEffect, например:
useEffect(() => {
// ...
}, [somePropery])
и это вызвало у меня бесконечный цикл, я попытался справиться с этим, передав весь something в качестве зависимости, и он работал правильно.
Основная проблема в том, что useEffect сравнивает входящее значение с текущим значением неглубоко. Это означает, что эти два значения сравниваются с использованием сравнения '===', которое проверяет только ссылки на объекты, и хотя значения массива и объекта совпадают, оно рассматривает их как два разных объекта. Я рекомендую вам ознакомиться с моим статья о useEffect как методах жизненного цикла.
Другое рабочее решение, которое я использовал для состояния массивов:
useEffect(() => {
setIngredients(ingredients.length ? ingredients : null);
}, [ingredients]);
Тобиас, какой вариант использования требует изменения значения ингредиентов после его изменения?