Нескінченний цикл у використанніЕфект


115

Я бавився з новою системою гачків у React 16.7-alpha і застряг у нескінченному циклі useEffect, коли стан, яким я обробляю, є об'єктом або масивом.

По-перше, я використовую useState і ініціюю його з таким порожнім об'єктом:

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

Потім, у useEffect, я використовую setObj, щоб знову встановити його в порожній об'єкт. В якості другого аргументу я передаю [obj], сподіваючись, що він не буде оновлюватися, якщо вміст об'єкта не змінився. Але воно постійно оновлюється. Я думаю, оскільки незалежно від змісту, це завжди різні об’єкти, що змушують React думати, що він постійно змінюється?

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

Те саме стосується масивів, але як примітив він не застряє в петлі, як очікувалося.

За допомогою цих нових гачків, як мені обробляти об’єкти та масив, перевіряючи, чи змінився вміст чи ні?


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

@Tobias, ти повинен прочитати мою відповідь. Я впевнений, що ви приймете це як правильну відповідь.
HalfWebDev

Відповіді:


128

Передача порожнього масиву як другого аргументу useEffect змушує його працювати лише під час монтування та демонтажу, таким чином зупиняючи будь-які нескінченні цикли.

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

Це мені було роз’яснено в дописі в блозі про гачки React на https://www.robinwieruch.de/react-hooks/


1
Це фактично порожній масив, а не порожній об'єкт, який ви повинні
ввести

20
Це не вирішує проблему, вам необхідно пройти залежності , використовувані гачку
Helado

1
Зверніть увагу, що при відключенні ефект буде запускати функцію очищення, якщо ви вказали її. Фактичний ефект не діє при відключенні. responsejs.org/docs/hooks-effect.html#example-using-hooks-1
tony

2
Використання порожнього масиву, коли setIngrediets - це залежність - це антипаттерн, як сказав Ден Абрамов. Не обробляйте useEffetct як метод componentDidMount ()
p7adams

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

78

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

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

  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
  }, [] )

Щоб запустити що-небудь один раз під час монтування компонентів та зміни даних / 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) 

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


@endavid залежить від того, який саме реквізит ви використовуєте
ZiiMakc

1
звичайно, якщо ви не використовуєте жодних значень із області дії компонента, тоді це можна безпечно опустити. Але суто з точки зору проектування / архітектури, це не є хорошою практикою, оскільки вимагає переміщення всієї функції всередину ефекту, якщо вам потрібно використовувати реквізит, і ви можете отримати метод useEffect, який буде використовувати обурлива кількість рядків коду.
egdavid

18

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

Якщо ви передасте об'єкт, 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]);

4
Інший підхід полягає в створенні таких значень з useMemo, таким чином посилання зберігається і значення залежностей оцінюються як же
Helado

@helado, ви можете використовувати useMemo () для значень або useCallback () для функцій
Juanma Menendez

Це випадкові значення передаються в масив залежностей? Звичайно це запобігає нескінченному циклу, але чи заохочується це? Я також міг би перейти 0до масиву залежностей?
thinkvantagedu

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

Дякую! Для мене це було неочевидно і саме проблема, з якою я стикався.
stuckj

1
Ти врятував мій день! Дякую, брате.
Han Van Pham,

7

Я не впевнений, що це буде працювати для вас, але ви можете спробувати додати .length так:

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

У моєму випадку (я отримував масив!) Він отримував дані при монтуванні, потім знову лише при зміні, і це не входило в цикл.


1
Що робити, якщо елемент замінено в масиві? У цьому випадку довжина масиву була б однаковою, і ефект не запускався!
Аамір Хан,

7

Якщо ви включите порожній масив в кінці useEffect :

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

Він би працював один раз.

Якщо ви також включите параметр для масиву:

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

Він запускався, коли змінюється текстовий параметр.


чому він запускається лише один раз, якщо ми покладемо порожній масив в кінець хука?
Карен

Порожній масив в кінці useEffect - це цілеспрямована реалізація розробниками для зупинки нескінченних циклів у ситуаціях, коли, наприклад, вам може знадобитися встановити State усередині useEffect. В іншому випадку це призвело б до useEffect -> оновлення стану -> useEffect -> нескінченний цикл.
to240

Порожній масив викликає попередження від eslinter.
hmcclungiii

4

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

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

setIngredients({});змінить значення ingredients(кожного разу повертатиме нове посилання), яке буде виконуватися setIngredients({}). Щоб вирішити це, ви можете використовувати будь-який із підходів:

  1. Передайте інший другий аргумент, щоб використовуватиEffect
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 (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()
      }
     
  })


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

0

Ви також можете деструктурувати об'єкт у масиві залежностей, тобто стан буде оновлюватися лише тоді, коли оновляться певні частини об'єкта.

Для цього прикладу, скажімо, інгредієнти містили моркву, ми могли б передати це залежності, і лише у разі зміни моркви стан держави оновився б.

Потім ви можете продовжувати це і лише оновлювати кількість моркви в певних точках, таким чином контролюючи час оновлення штату та уникаючи нескінченної петлі.

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

Прикладом того, як щось подібне може бути використано, є те, коли користувач входить на веб-сайт. Коли вони входять в систему, ми можемо деструктурувати об’єкт користувача для вилучення їхніх файлів cookie та дозволів та відповідно оновити стан програми.


0

мій випадок був особливим при зустрічі з нескінченною петлею, сенаріо було таким:

У мене був об’єкт, скажімо, objX, який походить від реквізиту, і я деструктурував його в реквізиті, як:

const { something: { somePropery } } = ObjX

і я використовував someProperyяк залежність від свого useEffectтипу:


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

і це викликало у мене нескінченний цикл, я намагався впоратися з цим, передаючи ціле somethingяк залежність, і воно працювало належним чином.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.