Бесконечный цикл в использовании

Я поигрался с новой системой ловушек в React 16.7-alpha и застрял в бесконечном цикле в useEffect, когда состояние, которое я обрабатываю, является объектом или массивом.

Сначала я использую useState и запускаю его с пустым объектом, например:

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

Затем в useEffect я использую setObj, чтобы снова установить для него пустой объект. В качестве второго аргумента я передаю [obj], надеясь, что он не обновится, если содержимое объекта не изменилось. Но он продолжает обновляться. Я думаю, потому что независимо от содержимого, это всегда разные объекты, заставляющие React думать, что он постоянно меняется?

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

То же самое и с массивами, но как примитив он не застревает в цикле, как ожидалось.

Используя эти новые хуки, как мне обрабатывать объекты и массив при проверке того, изменилось ли содержимое или нет?


person Tobias Haugen    schedule 30.10.2018    source источник
comment
Тобиас, какой вариант использования требует изменения значения ингредиентов после его изменения?   -  person Ben Carp    schedule 29.06.2019
comment
@Tobias, прочти мой ответ. Я уверен, что вы примете это как правильный ответ.   -  person HalfWebDev    schedule 13.08.2020
comment
Я прочитал эту статью, и она помогла мне понять что-то более понятное. Что бы я сделал, так это поиск определенных атрибутов объекта / массива, таких как количество элементов или имя (как вам угодно), и использовал бы их как зависимости в крючке useEffect.   -  person emalinga    schedule 08.02.2021


Ответы (15)


Передача пустого массива в качестве второго аргумента для useEffect заставляет его работать только при монтировании и размонтировании, тем самым останавливая любые бесконечные циклы.

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

Это было разъяснено мне в сообщении блога о перехватчиках React по адресу https://www.robinwieruch.de/react-hooks/

person Tobias Haugen    schedule 30.10.2018
comment
На самом деле это пустой массив, а не пустой объект, который вы должны передать. - person GifCo; 21.02.2019
comment
Это не решает проблему, вам нужно передать зависимости, используемые хуком. - person helado; 08.07.2019
comment
Обратите внимание, что при отключении эффект запустит функцию очистки, если вы ее указали. Фактический эффект не работает при размонтировании. reactjs.org/docs/hooks-effect.html#example- using-hooks-1 - person tony; 23.07.2019
comment
Как сказал Дэн Абрамов, использование пустого массива, когда setIngrediets является зависимостью, является антипаттерном. Не относитесь к useEffetct как к методу componentDidMount () - person p7adams; 15.12.2019
comment
НЕ ИСПОЛЬЗУЙТЕ ЕГО. Вы получите исчерпывающую ошибку deps: github.com/facebook/react/issues/14920 - person Alexander Kim; 24.02.2020
comment
поскольку приведенные выше люди не предоставили ссылку или ресурс о том, как правильно решить эту проблему, я предлагаю встречным любителям уток проверить это, поскольку они устранили мою проблему с бесконечным циклом: stackoverflow.com/questions/56657907/ - person C. Rib; 30.06.2020
comment
Можете ли вы помочь мне с подобной проблемой? stackoverflow.com/questions/67773860/ - person Peters_; 09.06.2021

Была такая же проблема. Я не знаю, почему они не упоминают об этом в документации. Просто хочу добавить немного к ответу Тобиаса Хогена.

Чтобы запустить каждый компонент / родительский рендеринг, вам необходимо использовать:

  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) 

  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>  
}
person ZiiMakc    schedule 31.10.2018
comment
Согласно часто задаваемым вопросам React, исключать функции из списка зависимостей небезопасно. - person egdavid; 01.08.2019
comment
@endavid зависит от того, какую опору вы используете - person ZiiMakc; 09.08.2019
comment
конечно, если вы не используете какие-либо значения из области действия компонента, то можно безопасно опустить. Но с чисто точки зрения проектирования / архитектуры vue это не очень хорошая практика, поскольку она требует, чтобы вы переместили всю функцию внутри эффекта, если вам нужно использовать props, и вы можете получить метод useEffect, который будет использовать невероятное количество строк кода. - person egdavid; 10.08.2019
comment
Это действительно полезно, поскольку вызов асинхронной функции внутри хука useEffect обновляется только при изменении значения. Спасибо друг - person EMMANUEL OKELLO; 03.03.2021

Я тоже однажды столкнулся с той же проблемой и исправил ее, убедившись, что я передаю примитивные значения во втором аргументе [].

Если вы передадите объект, React сохранит только ссылку на объект и запустит эффект при изменении ссылки, что обычно происходит каждый раз (хотя я сейчас не знаю, как это сделать).

Решение состоит в том, чтобы передать значения в объект. Ты можешь попробовать,

const obj = { keyA: 'a', keyB: 'b' }

useEffect(() => {
  // do something
}, [Object.values(obj)]);

or

const obj = { keyA: 'a', keyB: 'b' }

useEffect(() => {
  // do something
}, [obj.keyA, obj.keyB]);
person Dinesh Pandiyan    schedule 09.02.2019
comment
Другой подход заключается в создании таких значений с помощью useMemo, при этом ссылка сохраняется, а значения зависимостей оцениваются как те же самые. - person helado; 08.07.2019
comment
@helado вы можете использовать useMemo () для значений или useCallback () для функций - person Juanma Menendez; 09.12.2019
comment
Это случайные значения передаются в массив зависимостей? Конечно, это предотвращает бесконечный цикл, но поощряется ли это? Я мог бы передать 0 и в массив зависимостей? - person thinkvantagedu; 04.12.2020

Если вы создаете собственный крючок, вы можете иногда вызвать бесконечный цикл со значением по умолчанию следующим образом

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 = {}
}
person Jkarttunen    schedule 29.10.2019
comment
Спасибо! Для меня это было неочевидно, и именно с этой проблемой я столкнулся. - person stuckj; 24.01.2020

Как сказано в документации (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

В качестве второго аргумента можно передать любой объект. Если этот объект не изменится, ваш эффект сработает только после первого монтирования. Если объект изменится, эффект сработает снова.

person Rocío García Luque    schedule 11.02.2019

Если вы добавите пустой массив в конец useEffect:

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

Он бы запустился один раз.

Если вы также включите параметр в массив:

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

Он будет запускаться при изменении текстового параметра.

person besartm    schedule 02.03.2020
comment
почему он будет запускаться только один раз, если мы поместим пустой массив в конец хука? - person Karen; 22.06.2020
comment
Пустой массив в конце useEffect - это целенаправленная реализация разработчиками для остановки бесконечных циклов в ситуациях, когда вам, например, может потребоваться setState внутри useEffect. В противном случае это привело бы к useEffect - ›обновлению состояния -› useEffect - ›бесконечному циклу. - person to240; 24.07.2020
comment
Пустой массив вызывает предупреждение от eslinter. - person hmcclungiii; 04.01.2021
comment
Вот подробное объяснение этого: reactjs. org / docs / - person Franchy; 20.01.2021

Я не уверен, что это сработает для вас, но вы можете попробовать добавить .length следующим образом:

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

В моем случае (я получал массив!) Он получал данные при монтировании, затем снова только при изменении, и это не входило в цикл.

person Luka M    schedule 27.04.2019
comment
Что, если в массиве заменить элемент? В этом случае длина массива будет такой же, и эффект не будет работать! - person Aamir Khan; 07.08.2020

Ваш бесконечный цикл вызван круговоротом

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));
person Ben Carp    schedule 29.06.2019

Если вы используете эту оптимизацию, убедитесь, что в массив включены все значения из области действия компонента (такие как свойства и состояние), которые меняются с течением времени и используются эффектом.

Я считаю, что они пытаются выразить возможность того, что можно использовать устаревшие данные, и знать об этом. Не имеет значения, какой тип значений мы отправляем в 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]);

person locomotif    schedule 22.08.2019

Лучший способ - сравнить предыдущее значение с текущим значением с помощью usePrevious () и _.isEqual () из Lodash. Импортируйте isEqual и useRef. Сравните свое предыдущее значение с текущим значением в useEffect (). Если они такие же, больше ничего не обновляйте. usePrevious (value) - это настраиваемый обработчик, который создает ref с помощью 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()
      }
     
  })

person H S Progr    schedule 06.09.2019
comment
Я предполагаю, что люди проголосовали против этого, потому что вы предложили использовать стороннюю библиотеку. Однако основная идея хороша. - person jperl; 21.08.2020

В случае, если вам НЕОБХОДИМО сравнить объект и когда он обновляется, вот крючок 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]));
person HalfWebDev    schedule 19.04.2020
comment
Откуда взялась функция isEqual? - person Josh Bowden; 17.03.2021

Вы также можете деструктурировать объект в массиве зависимостей, то есть состояние будет обновляться только при обновлении определенных частей объекта.

Для этого примера предположим, что ингредиенты содержат морковь, мы могли бы передать это в зависимость, и только если морковь изменилась, состояние обновилось.

Затем вы можете пойти дальше и обновлять количество морковок только в определенных точках, тем самым контролируя, когда будет обновляться состояние, и избегая бесконечного цикла.

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

Примером использования чего-то подобного является вход пользователя на веб-сайт. Когда они входят в систему, мы можем деструктурировать объект пользователя, чтобы извлечь их cookie и роль разрешений, и соответствующим образом обновить состояние приложения.

person to240    schedule 24.07.2020

Основная проблема заключается в том, что useEffect сравнивает входящее значение с текущим значением неглубоко. Это означает, что эти два значения сравниваются с использованием сравнения '===', которое проверяет только ссылки на объекты, и хотя значения массива и объекта совпадают, оно рассматривает их как два разных объекта. Я рекомендую вам ознакомиться с моим статья об useEffect как методах жизненного цикла.

person Ali Toshmatov    schedule 23.03.2021

мой случай был особенным при столкновении с бесконечным циклом, сенарио было таким:

У меня был объект, скажем, objX, который исходит из реквизита, и я деструктурировал его в реквизитах, например:

const { something: { somePropery } } = ObjX

и я использовал somePropery как зависимость от моего useEffect, например:


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

и это вызвало у меня бесконечный цикл, я попытался справиться с этим, передав весь something в качестве зависимости, и он работал правильно.

person a_m_dev    schedule 27.09.2020

Другое рабочее решение, которое я использовал для состояния массивов:

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