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

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

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

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

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

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

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

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

Тобиас, какой вариант использования требует изменения значения ингредиентов после его изменения?

Ben Carp 29.06.2019 16:19

@Tobias, прочти мой ответ. Я уверен, что вы примете это как правильный ответ.

HalfWebDev 13.08.2020 06:30

Я прочитал этот статья, и он помог мне кое-что понять более ясно. Что бы я сделал, так это поиск определенных атрибутов объекта / массива, таких как количество элементов или имя (как вам угодно), и использовал бы их как зависимости в хуке useEffect.

emalinga 08.02.2021 10:22
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
158
3
122 993
15
Перейти к ответу Данный вопрос помечен как решенный

Ответы 15

Ответ принят как подходящий

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

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

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

На самом деле это пустой массив, а не пустой объект, который вы должны передать.

GifCo 21.02.2019 16:54

Это не решает проблему, вам нужно передать зависимости, используемые хуком.

helado 08.07.2019 07:09

Обратите внимание, что при отключении эффект запустит функцию очистки, если вы ее указали. Фактический эффект не работает при размонтировании. reactjs.org/docs/hooks-effect.html#example-using-hooks-1

tony 23.07.2019 12:54

Как сказал Дэн Абрамов, использование пустого массива, когда setIngrediets является зависимостью, является антипаттерном. Не относитесь к useEffetct как к методу componentDidMount ()

p7adams 15.12.2019 14:37

НЕ ИСПОЛЬЗУЙТЕ ЕГО. Вы получите исчерпывающую ошибку deps: github.com/facebook/react/issues/14920

Alexander Kim 24.02.2020 18:46

поскольку приведенные выше люди не предоставили ссылку или ресурс о том, как правильно решить эту проблему, я предлагаю встречным любителям уток проверить это, поскольку это решило мою проблему с бесконечным циклом: stackoverflow.com/questions/56657907/…

C. Rib 30.06.2020 19:28

Можете ли вы помочь мне с подобной проблемой? stackoverflow.com/questions/67773860/…

Peters_ 09.06.2021 14:42

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

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

  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, небезопасно исключать функции из списка зависимостей.

egdavid 01.08.2019 04:48

@endavid зависит от того, какую опору вы используете

ZiiMakc 09.08.2019 12:29

конечно, если вы не используете значения любой из области действия компонента, то можно безопасно опустить. Но с чисто точки зрения проектирования / архитектуры vue это не очень хорошая практика, поскольку она требует, чтобы вы переместили всю функцию внутри эффекта, если вам нужно использовать props, и вы можете получить метод useEffect, который будет использовать невероятное количество строк кода.

egdavid 10.08.2019 21:13

Это действительно полезно, поскольку вызов асинхронной функции внутри хука useEffect обновляется только при изменении значения. Спасибо друг

EMMANUEL OKELLO 03.03.2021 07:04

@egdavid, тогда как правильно запускать определенный блок кода при обновлении значения? Использование UseEffect вызывает ошибку lint для включения значения в зависимость, но это вызывает бесконечный цикл, поэтому этот метод нельзя использовать

Embedded_Mugs 27.07.2021 12:12

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

Если вы передадите объект, 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 08.07.2019 07:29

@helado вы можете использовать useMemo () для значений или useCallback () для функций

Juanma Menendez 09.12.2019 15:49

Это случайные значения передаются в массив зависимостей? Конечно, это предотвращает бесконечный цикл, но поощряется ли это? Могу я передать и 0 в массив зависимостей?

thinkvantagedu 03.12.2020 23:29

@Dinesh первое решение будет работать, если вы добавите оператор распространения }, ...[Object.values(obj)]); ... или просто удалите квадратные скобки }, Object.values(obj)); ...

vtolentino 20.08.2021 21:44

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

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

Что делать, если в массиве заменить элемент? В этом случае длина массива будет такой же, и эффект не будет работать!

Aamir Khan 07.08.2020 08:05

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

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));

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()
      }
     
  })

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

jperl 21.08.2020 18:33

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

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 = {}
}

Спасибо! Для меня это было неочевидно, и именно с этой проблемой я столкнулся.

stuckj 24.01.2020 00:22

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

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

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

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

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

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

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

Karen 22.06.2020 11:55

Пустой массив в конце useEffect - это целенаправленная реализация разработчиками для остановки бесконечных циклов в ситуациях, когда вам, например, может потребоваться setState внутри useEffect. В противном случае это привело бы к useEffect -> обновление состояния -> useEffect -> бесконечный цикл.

to240 24.07.2020 12:09

Пустой массив вызывает предупреждение от eslinter.

hmcclungiii 04.01.2021 00:09

Вот подробное объяснение этого: reactjs.org/docs/…

Franchy 20.01.2021 15:58

Если вам НЕОБХОДИМО сравнивать объект и когда он обновляется, вот ловушка 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?

Josh Bowden 17.03.2021 15:46

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

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

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

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

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

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

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

const { something: { somePropery } } = ObjX

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


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

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

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

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

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

Другие вопросы по теме