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

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

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

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

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

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

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

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

Ответы

Ответ 1

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

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

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

Ответ 2

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

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

  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 does not 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 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>  
}

Ответ 3

Как сказано в документации (https://reactjs.org/docs/hooks-effect.html), useEffect предназначена для использования, когда вы хотите, чтобы какой-то код выполнялся после каждого рендеринга. Из документов:

Запускается ли useEffect после каждого рендера? Да!

Если вы хотите настроить это, вы можете следовать инструкциям, которые появятся позже на той же странице (https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects). По useEffect метод useEffect принимает второй аргумент, который React проверит, чтобы определить, нужно ли снова запускать эффект или нет.

useEffect(() => {
  document.title = 'You clicked ${count} times';
}, [count]); // Only re-run the effect if count changes

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

Ответ 4

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

Если вы передадите объект, 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]);

Ответ 5

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

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

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

Ответ 6

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

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

Ответ 7

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

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

setIngredients({}); изменит значение ingredients (каждый раз будет возвращать новую ссылку), что запустит setIngredients({}). Для решения этой проблемы вы можете использовать любой подход:

  1. Передайте другой второй аргумент для useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
  setIngredients({});
}, [timeToChangeIngrediants ]);

setIngrediants будет работать, когда timeToChangeIngrediants изменился.

  1. Я не уверен, какой вариант использования оправдывает изменение ингредиентов после его изменения. Но если это так, вы передаете Object.values(ingrediants) в качестве второго аргумента для использованияEffect.
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));

Ответ 8

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

function DisplayCounter(props) {
  const [result, setResult] = useState(0)

  useEffect(() => {
    setResult(result + 1)
    console.log(result)
  });

  return (
    <div>
      {result}
    </div> 
  )
}

Поэтому один из способов избежать этого - использовать приведенные выше предложения (передавая второй аргумент с пустым массивом). Другой способ - избежать изменения состояния внутри хука useEffect.

Ответ 10

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

Ниже приведен фрагмент моего кода. Я столкнулся с проблемой бесконечного цикла с обновлением данных с помощью firebase hook

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