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?
źródło
Odpowiedzi:
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/
źródło
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> }
źródło
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]);
źródło
0
do tablicy zależności?Jak wspomniano w dokumentacji ( https://reactjs.org/docs/hooks-effect.html ),
useEffect
hak jest przeznaczony do użycia, gdy chcesz, aby jakiś kod był wykonywany po każdym renderowaniu . Z dokumentów: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
useEffect
metoda 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.
źródło
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 = {} }
źródło
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ę.
źródło
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.
źródło
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 uruchomionasetIngredients({})
. Aby rozwiązać ten problem, możesz użyć jednej z metod:const timeToChangeIngrediants = ..... useEffect(() => { setIngredients({}); }, [timeToChangeIngrediants ]);
setIngrediants
będzie działać, gdytimeToChangeIngrediants
się zmieni.Object.values(ingrediants)
jako drugi argument do useEffect.useEffect(() => { setIngredients({}); }, Object.values(ingrediants));
źródło
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
array
drugim argumencie, o ile wiemy, że jeśli którakolwiek z tych wartości się zmieni, spowoduje to wykonanie efektu. Jeśli używamyingredients
jako części obliczenia w ramach efektu, powinniśmy uwzględnić to w plikuarray
.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]);
źródło
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() } })
źródło
W przypadku, gdy musisz porównać obiekt i kiedy jest on aktualizowany, tutaj znajduje się
deepCompare
hak 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
useEffect
hakuReact.useEffect(() => { setState(state); }, useDeepCompare([state]));
źródło
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.
źródło
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
somePropery
jako zależności od moichuseEffect
:useEffect(() => { // ... }, [somePropery])
i spowodowało to nieskończoną pętlę, próbowałem sobie z tym poradzić, przekazując całość
something
jako zależność i działało poprawnie.źródło