Я играл с новой системой хуков в React 16.7-alpha и застревал в бесконечном цикле в useEffect, когда состояние, которое я обрабатываю, является объектом или массивом.
Сначала я использую useState и инициирую его с помощью пустого объекта, например этого:
const [obj, setObj] = useState({});
Затем в useEffect я использую setObj, чтобы снова установить для него пустой объект. В качестве второго аргумента я передаю [obj], надеясь, что он не обновится, если содержимое объекта не изменилось. Но он продолжает обновляться. Я думаю, потому что независимо от содержимого, это всегда разные объекты, заставляющие React думать, что он постоянно меняется?
useEffect(() => {
setIngredients({});
}, [ingredients]);
То же самое и с массивами, но как примитив он не застревает в цикле, как ожидалось.
Как, используя эти новые хуки, обрабатывать объекты и массив, проверяя, изменилось ли содержимое или нет?
источник
Ответы:
Передача пустого массива в качестве второго аргумента для useEffect заставляет его работать только при монтировании и размонтировании, тем самым останавливая любые бесконечные циклы.
useEffect(() => { setIngredients({}); }, []);
Это было разъяснено мне в сообщении блога о перехватчиках React на https://www.robinwieruch.de/react-hooks/
источник
Была такая же проблема. Я не знаю, почему они не упоминают об этом в документации. Просто хочу добавить немного к ответу Тобиаса Хогена.
Для запуска в каждом компоненте / родительском рендере вам необходимо использовать:
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 }, [] )
Чтобы запустить что-либо один раз при монтировании компонента и при изменении данных / данных2 :
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) useEffect(() => { loadBookFromServer() }, [id]) // every time id changed, new book will be loaded // Remeber that it's 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> }
источник
Я тоже однажды столкнулся с той же проблемой и исправил ее, убедившись, что я передаю примитивные значения во втором аргументе
[]
.Если вы передадите объект, 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
ловушка предназначена для использования, когда вы хотите, чтобы некоторый код выполнялся после каждой визуализации . Из документов:Если вы хотите настроить это, вы можете выполнить инструкции, которые появятся позже на той же странице ( 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
В качестве второго аргумента можно передать любой объект. Если этот объект не изменится, ваш эффект сработает только после первого маунта . Если объект изменится, эффект сработает снова.
источник
Если вы создаете собственный хук , вы можете иногда вызвать бесконечный цикл со значением по умолчанию следующим образом
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 = {} }
источник
Я не уверен, сработает ли это для вас, но вы можете попробовать добавить .length следующим образом:
useEffect(() => { // fetch from server and set as obj }, [obj.length]);
В моем случае (я получал массив!) Он получал данные при монтировании, затем снова только при изменении и не входил в цикл.
источник
Если вы добавите пустой массив в конце useEffect :
useEffect(()=>{ setText(text); },[])
Он запустится один раз.
Если вы также включите параметр в массив:
useEffect(()=>{ setText(text); },[text])
Он будет запускаться при изменении текстового параметра.
источник
Ваш бесконечный цикл связан с округлостью
useEffect(() => { setIngredients({}); }, [ingredients]);
setIngredients({});
изменит значениеingredients
(каждый раз будет возвращать новую ссылку), который будет запущенsetIngredients({})
. Чтобы решить эту проблему, вы можете использовать любой подход:const timeToChangeIngrediants = ..... useEffect(() => { setIngredients({}); }, [timeToChangeIngrediants ]);
setIngrediants
будет работать, когдаtimeToChangeIngrediants
изменилось.Object.values(ingrediants)
в качестве второго аргумента useEffect.useEffect(() => { setIngredients({}); }, Object.values(ingrediants));
источник
Я считаю, что они пытаются выразить возможность того, что кто-то может использовать устаревшие данные, и знать об этом. Не имеет значения, какой тип значений мы отправляем
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 . Импортируйте isEqual и 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() } })
источник
В случае, если вам НЕОБХОДИМО сравнить объект и когда он обновляется, вот
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 и роль разрешений, и соответствующим образом обновить состояние приложения.
источник
мой случай был особенным при столкновении с бесконечным циклом, сенарио было таким:
У меня был объект, скажем, objX, который исходит из реквизита, и я деструктурировал его в реквизитах, например:
const { something: { somePropery } } = ObjX
и я использовал
somePropery
как зависимость от своегоuseEffect
лайка:useEffect(() => { // ... }, [somePropery])
и это вызвало у меня бесконечный цикл, я попытался справиться с этим, передав все
something
как зависимость, и он работал правильно.источник