Endlosschleife in useEffect


113

Ich habe mit dem neuen Hook-System in React 16.7-alpha herumgespielt und stecke in useEffect in einer Endlosschleife fest, wenn der Status, den ich verarbeite, ein Objekt oder Array ist.

Zuerst benutze ich useState und initiiere es mit einem leeren Objekt wie folgt:

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

Dann benutze ich in useEffect setObj, um es wieder auf ein leeres Objekt zu setzen. Als zweites Argument übergebe ich [obj] in der Hoffnung, dass es nicht aktualisiert wird, wenn sich der Inhalt des Objekts nicht geändert hat. Aber es wird ständig aktualisiert. Ich denke, weil dies unabhängig vom Inhalt immer unterschiedliche Objekte sind, die React denken lassen, dass es sich ständig ändert?

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

Das gleiche gilt für Arrays, aber als Grundelement bleibt es nicht wie erwartet in einer Schleife hängen.

Wie soll ich mit diesen neuen Hooks mit Objekten und Arrays umgehen, wenn überprüft wird, ob sich der Inhalt geändert hat oder nicht?


Tobias, in welchem ​​Anwendungsfall muss der Wert von Zutaten geändert werden, sobald sich der Wert geändert hat?
Ben Carp

@Tobias, du solltest meine Antwort lesen. Ich bin sicher, Sie werden es als die richtige Antwort akzeptieren.
HalfWebDev

Antworten:


127

Wenn Sie ein leeres Array als zweites Argument für useEffect übergeben, wird es nur beim Ein- und Aushängen ausgeführt, wodurch Endlosschleifen gestoppt werden.

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

Dies wurde mir im Blogbeitrag zu React Hooks unter https://www.robinwieruch.de/react-hooks/ klargestellt.


1
Es ist eigentlich ein leeres Array, kein leeres Objekt, das Sie übergeben sollten.
GifCo

19
Dies löst nicht das Problem, müssen Sie Abhängigkeiten von dem Haken verwendet passieren
Helado

1
Beachten Sie, dass der Effekt beim Aufheben der Bereitstellung eine Bereinigungsfunktion ausführt, wenn Sie eine angegeben haben. Der eigentliche Effekt wird beim Aufheben der Bereitstellung nicht ausgeführt. reactjs.org/docs/hooks-effect.html#example-using-hooks-1
Tony

2
Die Verwendung eines leeren Arrays, wenn setIngrediets eine Abhängigkeit ist, ist ein Antimuster, wie Dan Abramov sagte. Behandeln Sie useEffetct nicht wie eine componentDidMount () -Methode
p7adams

1
Da die oben genannten Personen keinen Link oder keine Ressource zur korrekten Lösung dieses Problems bereitgestellt haben, empfehle ich entgegenkommenden Entenenten, dies zu überprüfen, da dies mein Endlosschleifenproblem behoben hat
C. Rib

75

Hatte das gleiche Problem. Ich weiß nicht, warum sie dies in Dokumenten nicht erwähnen. Ich möchte nur ein wenig zur Antwort von Tobias Haugen hinzufügen.

Um in jeder Komponente / jedem übergeordneten Renderer ausgeführt zu werden , müssen Sie Folgendes verwenden:

  useEffect(() => {

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

Um etwas nur einmal nach dem Mounten der Komponente auszuführen (wird einmal gerendert), müssen Sie Folgendes verwenden:

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

So führen Sie etwas einmalig auf der Komponentenmontage und bei Daten- / Daten2- Änderungen aus:

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

Wie ich es die meiste Zeit benutze:

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 hängt davon ab, welche Requisite Sie verwenden
ZiiMakc

1
Wenn Sie keine Werte aus dem Komponentenbereich verwenden , können Sie diese natürlich weglassen. Aus rein gestalterischer Sicht ist dies jedoch keine gute Vorgehensweise, da Sie Ihre gesamte Funktion innerhalb des Effekts verschieben müssen, wenn Sie Requisiten verwenden müssen, und möglicherweise eine useEffect-Methode verwenden, die eine verwendet unverschämte Menge an Codezeilen.
Egdavid

18

Ich bin auch einmal auf dasselbe Problem gestoßen und habe es behoben, indem ich sichergestellt habe, dass ich im zweiten Argument primitive Werte übergebe [].

Wenn Sie ein Objekt übergeben, speichert React nur die Referenz auf das Objekt und führt den Effekt aus, wenn sich die Referenz ändert, was normalerweise jedes Mal der Fall ist (ich weiß jetzt aber nicht wie).

Die Lösung besteht darin, die Werte im Objekt zu übergeben. Du kannst es versuchen,

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

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

oder

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

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

4
Ein anderer Ansatz besteht darin, solche Werte mit useMemo zu erstellen. Auf diese Weise wird die Referenz beibehalten und die Werte der Abhängigkeiten werden als dieselben ausgewertet
helado

@helado Sie können useMemo () für Werte oder useCallback () für Funktionen verwenden
Juanma Menendez

Werden diese Zufallswerte an das Abhängigkeitsarray übergeben? Dies verhindert natürlich die Endlosschleife, aber wird dies gefördert? Ich könnte auch 0an das Abhängigkeitsarray übergeben?
thinkvantagedu

11

Wie in der Dokumentation ( https://reactjs.org/docs/hooks-effect.html ) angegeben, soll der useEffectHook verwendet werden, wenn nach jedem Rendern Code ausgeführt werden soll . Aus den Dokumenten:

Wird useEffect nach jedem Rendern ausgeführt? Ja!

Wenn Sie dies anpassen möchten, können Sie den Anweisungen folgen, die später auf derselben Seite angezeigt werden ( https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects ). Grundsätzlich useEffectakzeptiert die Methode ein zweites Argument , das React prüft, um festzustellen, ob der Effekt erneut ausgelöst werden muss oder nicht.

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

Sie können jedes Objekt als zweites Argument übergeben. Wenn dieses Objekt unverändert bleibt, wird Ihr Effekt erst nach dem ersten Mount ausgelöst . Wenn sich das Objekt ändert, wird der Effekt erneut ausgelöst.


11

Wenn Sie einen benutzerdefinierten Hook erstellen , können Sie manchmal eine Endlosschleife mit der folgenden Standardeinstellung verursachen

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

Die Lösung besteht darin, dasselbe Objekt zu verwenden, anstatt bei jedem Funktionsaufruf ein neues zu erstellen:

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

Wenn Sie in Ihrem Komponentencode darauf stoßen, wird die Schleife möglicherweise behoben, wenn Sie defaultProps anstelle von ES6-Standardwerten verwenden

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

MyComponent.defaultProps = {
  values = {}
}

Vielen Dank! Dies war für mich nicht offensichtlich und genau das Problem, auf das ich stieß.
stecken

1
Du hast meinen Tag gerettet! Dank bro.
Han Van Pham

6

Ich bin nicht sicher, ob dies für Sie funktioniert, aber Sie könnten versuchen, .length wie folgt hinzuzufügen:

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

In meinem Fall (ich habe ein Array abgerufen!) Wurden Daten beim Mounten abgerufen, dann wieder nur beim Ändern, und es ging nicht in eine Schleife.


1
Was ist, wenn ein Element im Array ersetzt wird? In diesem Fall wäre die Länge des Arrays gleich und der Effekt würde nicht ausgeführt!
Aamir Khan

6

Wenn Sie am Ende von useEffect ein leeres Array einfügen :

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

Es würde einmal laufen.

Wenn Sie auch Parameter in das Array aufnehmen:

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

Es wird immer dann ausgeführt, wenn sich die Textparameter ändern.


Warum würde es nur einmal ausgeführt, wenn wir ein leeres Array am Ende eines Hooks platzieren?
Karen

Ein leeres Array am Ende eines useEffect ist eine gezielte Implementierung der Entwickler, um Endlosschleifen in Situationen zu stoppen, in denen Sie beispielsweise den Status innerhalb eines useEffect festlegen müssen. Dies würde sonst zu useEffect -> Statusaktualisierung -> useEffect -> Endlosschleife führen.
240

4

Ihre Endlosschleife ist auf Zirkularität zurückzuführen

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

setIngredients({});ändert den Wert von ingredients(gibt jedes Mal eine neue Referenz zurück), der ausgeführt wird setIngredients({}). Um dies zu lösen, können Sie einen der folgenden Ansätze verwenden:

  1. Übergeben Sie ein anderes zweites Argument an useEffect
const timeToChangeIngrediants = .....
useEffect(() => {
  setIngredients({});
}, [timeToChangeIngrediants ]);

setIngrediantswird ausgeführt, wenn timeToChangeIngrediantssich geändert hat.

  1. Ich bin mir nicht sicher, welcher Anwendungsfall eine Änderung der Inhaltsstoffe nach einer Änderung rechtfertigt. In diesem Fall übergeben Sie Object.values(ingrediants)als zweites Argument useEffect.
useEffect(() => {
  setIngredients({});
}, Object.values(ingrediants));

2

Wenn Sie diese Optimierung verwenden, stellen Sie sicher, dass das Array alle Werte aus dem Komponentenbereich (z. B. Requisiten und Status) enthält, die sich im Laufe der Zeit ändern und vom Effekt verwendet werden.

Ich glaube, sie versuchen, die Möglichkeit auszudrücken, dass man veraltete Daten verwenden könnte, und sich dessen bewusst zu sein. Es spielt keine Rolle, welche Art von Werten wir arrayfür das zweite Argument senden, solange wir wissen, dass der Effekt ausgeführt wird, wenn sich einer dieser Werte ändert. Wenn wir ingredientsals Teil der Berechnung innerhalb des Effekts verwenden, sollten wir ihn in die 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

Der beste Weg ist, den vorherigen Wert mit dem aktuellen Wert zu vergleichen, indem Sie usePrevious () und _.isEqual () von Lodash verwenden . Import isEqual und useRef . Vergleichen Sie Ihren vorherigen Wert mit dem aktuellen Wert in useEffect () . Wenn sie gleich sind, aktualisieren Sie nichts anderes. usePrevious (Wert) ist ein benutzerdefinierter Haken , die einen schafft ref mit useRef () .

Unten ist ein Ausschnitt meines Codes. Ich hatte das Problem einer Endlosschleife beim Aktualisieren von Daten mithilfe des Firebase-Hooks

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
Ich denke, die Leute haben dies abgelehnt, weil Sie vorgeschlagen haben, eine Bibliothek eines Drittanbieters zu verwenden. Die zugrunde liegende Idee ist jedoch gut.
Jperl

1

Falls Sie das Objekt vergleichen müssen und wenn es aktualisiert wird, finden Sie hier einen deepCompareHaken zum Vergleich. Die akzeptierte Antwort spricht das sicherlich nicht an. Ein []Array ist geeignet, wenn der Effekt beim Mounten nur einmal ausgeführt werden soll.

Andere abgestimmte Antworten befassen sich nur mit einer Prüfung auf primitive Typen, indem sie obj.valueetwas Ähnliches tun , um zuerst die Ebene zu erreichen, auf der sie nicht verschachtelt sind. Dies ist möglicherweise nicht der beste Fall für tief verschachtelte Objekte.

Hier ist also eine, die in allen Fällen funktioniert.

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

Sie können das gleiche im useEffectHaken verwenden

React.useEffect(() => {
        setState(state);
    }, useDeepCompare([state]));

0

Sie können das Objekt auch im Abhängigkeitsarray zerstören, dh der Status wird nur aktualisiert, wenn bestimmte Teile des Objekts aktualisiert werden.

Nehmen wir für dieses Beispiel an, die Zutaten enthielten Karotten. Wir könnten dies an die Abhängigkeit übergeben, und nur wenn sich die Karotten ändern, wird der Status aktualisiert.

Sie könnten dies dann weiter verfolgen und die Anzahl der Karotten nur an bestimmten Punkten aktualisieren, um so zu steuern, wann der Status aktualisiert wird, und eine Endlosschleife zu vermeiden.

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

Ein Beispiel dafür, wann so etwas verwendet werden könnte, ist die Anmeldung eines Benutzers bei einer Website. Wenn sie sich anmelden, können wir das Benutzerobjekt zerstören, um ihre Cookie- und Berechtigungsrolle zu extrahieren und den Status der App entsprechend zu aktualisieren.


0

Mein Fall war etwas Besonderes bei der Begegnung mit einer Endlosschleife. Das Senario war wie folgt:

Ich hatte ein Objekt, sagen wir objX, das von Requisiten stammt, und ich habe es in Requisiten wie:

const { something: { somePropery } } = ObjX

und ich habe das someProperyals abhängigkeit zu meinem useEffectlike benutzt:


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

und es verursachte mir eine Endlosschleife, ich versuchte dies zu handhaben, indem ich das Ganze somethingals Abhängigkeit übergab und es funktionierte richtig.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.