Nieskończona pętla w useEffect

115

Bawiłem się nowym systemem przechwytywania w React 16.7-alpha i utknąłem w nieskończonej pętli w useEffect, gdy obsługiwany stan jest obiektem lub tablicą.

Najpierw używam useState i inicjuję go z pustym obiektem, takim jak ten:

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

Następnie w useEffect używam setObj, aby ponownie ustawić go na pusty obiekt. Jako drugi argument podaję [obj], mając nadzieję, że nie zaktualizuje się, jeśli zawartość obiektu nie uległa zmianie. Ale ciągle się aktualizuje. Wydaje mi się, że niezależnie od treści, zawsze są to różne obiekty, przez co React myśli, że ciągle się zmienia?

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

To samo dotyczy tablic, ale jako prymityw nie utknie w pętli, zgodnie z oczekiwaniami.

Korzystając z tych nowych punktów zaczepienia, jak mam obsługiwać obiekty i tablicę podczas sprawdzania, czy zawartość uległa zmianie, czy nie?

Tobias Haugen
źródło
Tobiaszu, jaki przypadek użycia wymaga zmiany wartości składników, gdy ich wartość uległa zmianie?
Ben Carp
@Tobias, powinieneś przeczytać moją odpowiedź. Jestem pewien, że zaakceptujesz to jako poprawną odpowiedź.
HalfWebDev

Odpowiedzi:

128

Przekazanie pustej tablicy jako drugiego argumentu funkcji useEffect powoduje, że działa on tylko przy montowaniu i odmontowywaniu, co powoduje zatrzymanie wszelkich nieskończonych pętli.

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

Zostało to wyjaśnione w poście na blogu dotyczącym haków Reacta pod adresem https://www.robinwieruch.de/react-hooks/

Tobias Haugen
źródło
1
W rzeczywistości jest to pusta tablica, a nie pusty obiekt, który powinieneś przekazać.
GifCo
20
To nie rozwiązuje problemu, musisz przekazać zależności używane przez hook
helado,
1
Zauważ, że po odmontowaniu efekt uruchomi funkcję czyszczenia, jeśli ją podałeś. Rzeczywisty efekt nie działa po odmontowaniu. reakjs.org/docs/hooks-effect.html#example-using-hooks-1
tony
2
Używanie pustej tablicy, gdy setIngrediets jest zależnością, jest przeciwieństwem, jak powiedział Dan Abramov. Nie traktuj metody useEffetct jak metody componentDidMount ()
p7adams,
1
ponieważ osoby powyżej nie dostarczyły linku ani zasobu na temat prawidłowego rozwiązania tego problemu, sugeruję nadjeżdżającym kaczkoludom to sprawdzić, ponieważ naprawiło to mój problem z nieskończoną pętlą: stackoverflow.com/questions/56657907/ ...
C. Żebro
78

Miałem ten sam problem. Nie wiem, dlaczego oni nie wspominają o tym w dokumentach. Chcę tylko dodać trochę do odpowiedzi Tobiasa Haugena.

Aby uruchomić w każdym wyrejestrowaniu komponentu / elementu nadrzędnego , musisz użyć:

  useEffect(() => {

    // don't know where it can be used :/
  })

Aby uruchomić cokolwiek tylko raz po zamontowaniu komponentu (zostanie wyrenderowane raz), musisz użyć:

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

Aby uruchomić cokolwiek raz po zamontowaniu komponentu i zmianie danych / danych2 :

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

Jak go używam przez większość czasu:

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
źródło
2
Zgodnie z najczęściej zadawanymi pytaniami dotyczącymi Reacta pomijanie funkcji z listy zależności nie jest bezpieczne .
egdavid
@endavid zależy od tego, jakiego rekwizytu używasz
ZiiMakc
1
oczywiście, jeśli nie używasz żadnych wartości z zakresu komponentu , możesz bezpiecznie pominąć. Ale z punktu widzenia czysto projektowego / architektonicznego nie jest to dobra praktyka, ponieważ wymaga przeniesienia całej funkcji wewnątrz efektu, jeśli potrzebujesz użyć rekwizytów, i możesz skończyć z metodą useEffect, która będzie używać oburzająca ilość wierszy kodu.
egdavid
18

Raz napotkałem ten sam problem i naprawiłem go, upewniając się, że w drugim argumencie przekazuję prymitywne wartości [].

Jeśli przekażesz obiekt, React zapisze tylko odniesienie do obiektu i uruchomi efekt, gdy odniesienie się zmieni, czyli zwykle za każdym razem (nie wiem jak).

Rozwiązaniem jest przekazanie wartości w obiekcie. Możesz spróbować,

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

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

lub

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

useEffect(() => {
  // do something
}, [obj.keyA, obj.keyB]);
Dinesh Pandiyan
źródło
4
Innym podejściem jest utworzenie takich wartości za pomocą useMemo, dzięki czemu referencja jest zachowywana, a wartości zależności są oceniane jako takie same
helado.
@helado możesz użyć useMemo () dla wartości lub useCallback () dla funkcji
Juanma Menendez
Czy to losowe wartości są przekazywane do tablicy zależności? Oczywiście zapobiega to nieskończonej pętli, ale czy jest to zalecane? Mógłbym również przejść 0do tablicy zależności?
thinkvantagedu
11

Jak wspomniano w dokumentacji ( https://reactjs.org/docs/hooks-effect.html ), useEffecthak jest przeznaczony do użycia, gdy chcesz, aby jakiś kod był wykonywany po każdym renderowaniu . Z dokumentów:

Czy useEffect działa po każdym renderowaniu? Tak!

Jeśli chcesz to dostosować, możesz postępować zgodnie z instrukcjami, które pojawią się później na tej samej stronie ( https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects ). Zasadniczo useEffectmetoda przyjmuje drugi argument , który React zbada, aby określić, czy efekt musi zostać uruchomiony ponownie, czy nie.

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

Jako drugi argument można przekazać dowolny obiekt. Jeśli ten obiekt pozostanie niezmieniony, efekt zostanie aktywowany dopiero po pierwszym wierzchowcu . Jeśli obiekt się zmieni, efekt zostanie uruchomiony ponownie.

Rocío García Luque
źródło
11

Jeśli tworzysz niestandardowy punkt zaczepienia , czasami możesz wywołać nieskończoną pętlę z domyślnymi ustawieniami w następujący sposób

function useMyBadHook(values = {}) {
    useEffect(()=> { 
           /* This runs every render, if values is undefined */
        },
        [values] 
    )
}

Rozwiązaniem jest użycie tego samego obiektu zamiast tworzenia nowego przy każdym wywołaniu funkcji:

const defaultValues = {};
function useMyBadHook(values = defaultValues) {
    useEffect(()=> { 
           /* This runs on first call and when values change */
        },
        [values] 
    )
}

Jeśli napotkasz to w kodzie komponentu, pętla może zostać naprawiona, jeśli użyjesz defaultProps zamiast domyślnych wartości ES6

function MyComponent({values}) {
  useEffect(()=> { 
       /* do stuff*/
    },[values] 
  )
  return null; /* stuff */
}

MyComponent.defaultProps = {
  values = {}
}
Jkarttunen
źródło
Dzięki! To było dla mnie nieoczywiste i dokładnie to, z czym miałem do czynienia.
stuckj
1
Uratowałeś mi dzień! Dzięki stary.
Han Van Pham
7

Nie jestem pewien, czy to zadziała, ale możesz spróbować dodać .length w ten sposób:

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

W moim przypadku (pobierałem tablicę!) Pobierał dane na montowanie, potem znowu tylko przy zmianie i nie zapętlał się.

Luka M
źródło
1
Co się stanie, jeśli element zostanie zastąpiony w tablicy? W takim przypadku długość tablicy byłaby taka sama, a efekt nie działałby!
Aamir Khan
7

Jeśli dołączysz pustą tablicę na końcu useEffect :

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

Raz by się uruchomił.

Jeśli dołączysz również parametr do tablicy:

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

Byłoby uruchamiane przy każdej zmianie parametru tekstowego.

besartm
źródło
dlaczego miałby działać tylko raz, gdybyśmy umieścili pustą tablicę na końcu hooka?
Karen
Pusta tablica na końcu useEffect jest celową implementacją przez programistów, aby zatrzymać nieskończone pętle w sytuacjach, w których możesz na przykład potrzebować setState wewnątrz useEffect. W przeciwnym razie doprowadziłoby to do useEffect -> aktualizacja stanu -> useEffect -> nieskończona pętla.
240
Pusta tablica powoduje ostrzeżenie od eslintera.
hmcclungiii
4

Twoja nieskończona pętla jest spowodowana kołowością

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

setIngredients({});zmieni wartość ingredients(za każdym razem zwróci nowe odwołanie), która zostanie uruchomiona setIngredients({}). Aby rozwiązać ten problem, możesz użyć jednej z metod:

  1. Przekaż inny drugi argument do useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
  setIngredients({});
}, [timeToChangeIngrediants ]);

setIngrediantsbędzie działać, gdy timeToChangeIngrediantssię zmieni.

  1. Nie jestem pewien, jaki przypadek użycia uzasadnia zmianę składników po zmianie. Ale jeśli tak jest, przekazujesz Object.values(ingrediants)jako drugi argument do useEffect.
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));
Ben Carp
źródło
2

Jeśli używasz tej optymalizacji, upewnij się, że tablica zawiera wszystkie wartości z zakresu komponentu (takie jak właściwości i stan), które zmieniają się w czasie i są używane przez efekt.

Uważam, że próbują wyrazić możliwość korzystania z nieaktualnych danych i mieć tego świadomość. Nie ma znaczenia, jaki typ wartości wysyłamy w arraydrugim argumencie, o ile wiemy, że jeśli którakolwiek z tych wartości się zmieni, spowoduje to wykonanie efektu. Jeśli używamy ingredientsjako części obliczenia w ramach efektu, powinniśmy uwzględnić to w pliku 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]);

locomotif
źródło
1

Najlepszym sposobem jest porównanie poprzedniej wartości z wartością bieżącą przy użyciu funkcji usePrevious () i _.isEqual () z Lodash . Import jest równy i useRef . Porównaj swoją poprzednią wartość z bieżącą wartością wewnątrz funkcji useEffect () . Jeśli są takie same, nic więcej nie aktualizuj. usePrevious (value) to niestandardowy punkt zaczepienia, który tworzy ref za pomocą useRef () .

Poniżej znajduje się fragment mojego kodu. Miałem do czynienia z problemem nieskończonej pętli podczas aktualizacji danych za pomocą haka 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
źródło
1
Myślę, że ludzie odrzucili to, ponieważ zasugerowałeś użycie biblioteki innej firmy. Jednak podstawowa idea jest dobra.
jperl
1

W przypadku, gdy musisz porównać obiekt i kiedy jest on aktualizowany, tutaj znajduje się deepComparehak do porównania. Przyjęta odpowiedź z pewnością tego nie rozwiązuje. Posiadanie[] tablicy jest odpowiednie, jeśli chcesz, aby efekt działał tylko raz po zamontowaniu.

Ponadto inne głosowane odpowiedzi odnoszą się tylko do sprawdzenia typów pierwotnych przez wykonanie obj.value lub coś podobnego, aby najpierw dostać się do poziomu, na którym nie jest zagnieżdżona. Może to nie być najlepszy przypadek w przypadku obiektów głęboko zagnieżdżonych.

Więc tutaj jest taki, który zadziała we wszystkich przypadkach.

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

Możesz użyć tego samego w useEffecthaku

React.useEffect(() => {
        setState(state);
    }, useDeepCompare([state]));
HalfWebDev
źródło
0

Możesz także zniszczyć obiekt w tablicy zależności, co oznacza, że ​​stan będzie aktualizowany tylko wtedy, gdy zaktualizują się niektóre części obiektu.

Na potrzeby tego przykładu, powiedzmy, że składniki zawierały marchewki, moglibyśmy przekazać to do zależności i tylko w przypadku zmiany marchwi stan zaktualizowałby się.

Następnie możesz pójść dalej i zaktualizować liczbę marchewek tylko w określonych punktach, kontrolując w ten sposób, kiedy stan się aktualizuje i unikając nieskończonej pętli.

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

Przykładem sytuacji, w której można użyć czegoś takiego, jest sytuacja, w której użytkownik loguje się na stronie internetowej. Gdy się logują, możemy zniszczyć obiekt użytkownika, aby wyodrębnić jego rolę pliku cookie i uprawnień oraz odpowiednio zaktualizować stan aplikacji.

to240
źródło
0

mój przypadek był wyjątkowy w napotkaniu nieskończonej pętli, senario wyglądał tak:

Miałem obiekt, powiedzmy, że objX pochodzi z rekwizytów i niszczę go w rekwizytach takich jak:

const { something: { somePropery } } = ObjX

i użyłem someProperyjako zależności od moich useEffect:


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

i spowodowało to nieskończoną pętlę, próbowałem sobie z tym poradzić, przekazując całość somethingjako zależność i działało poprawnie.

a_m_dev
źródło