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

105

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

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

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

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

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

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

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

Тобиас Хауген
источник
Тобиас, какой вариант использования требует изменения значения ингредиентов после его изменения?
Бен Карп
@ Тобиас, прочти мой ответ. Я уверен, что вы примете это как правильный ответ.
HalfWebDev

Ответы:

123

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

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

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

Тобиас Хауген
источник
1
На самом деле это пустой массив, а не пустой объект, который вы должны передать.
GifCo
16
Это не решает проблему, вам нужно передать зависимости, используемые хуком
helado 08
1
Обратите внимание, что при размонтировании эффект запустит функцию очистки, если вы ее указали. Фактический эффект не работает при размонтировании. reactjs.org/docs/hooks-effect.html#example-using-hooks-1
Тони,
2
Использование пустого массива, когда setIngrediets является зависимостью, является антипаттерном, как сказал Дэн Абрамов. Не относитесь к useEffetct как к методу componentDidMount ()
p7adams
1
поскольку приведенные выше люди не предоставили ссылку или ресурс о том, как правильно решить эту проблему, я предлагаю встречным duckduckgoers проверить это, поскольку это устранило мою проблему с бесконечным циклом: stackoverflow.com/questions/56657907/…
C. Rib
72

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

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

  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>  
}
ZiiMakc
источник
@endavid зависит от того, какую опору вы используете
ZiiMakc
1
конечно, если вы не используете какие-либо значения из области действия компонента, то можно безопасно пропустить. Но с чисто точки зрения проектирования / архитектуры vue это не очень хорошая практика, поскольку она требует, чтобы вы переместили всю функцию внутри эффекта, если вам нужно использовать props, и вы можете получить метод useEffect, который будет использовать невероятное количество строк кода.
egdavid
16

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

Если вы передадите объект, 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]);
Динеш Пандиян
источник
3
Другой подход - создать такие значения с помощью useMemo, чтобы ссылка сохранялась, а значения зависимостей оценивались одинаково
helado
@helado, вы можете использовать useMemo () для значений или useCallback () для функций
Хуанма Менендес,
11

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

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

Росио Гарсиа Луке
источник
11

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

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 = {}
}
Jkarttunen
источник
Благодарность! Для меня это было неочевидно, и именно с этой проблемой я столкнулся.
stuckj
1
Ты спас мне день! Спасибо, бро.
Хан Ван Фам,
6

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

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

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

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

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

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

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

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

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

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

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

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

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));
Бен Карп
источник
2

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

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

локомотив
источник
1

Лучший способ - сравнить предыдущее значение с текущим значением с помощью 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()
      }
     
  })

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

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

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

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

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

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

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

to240
источник
0

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

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

const { something: { somePropery } } = ObjX

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


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

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

a_m_dev
источник