Hook-Warnungen für asynchrone Funktion in useEffect reagieren: Die useEffect-Funktion muss eine Bereinigungsfunktion oder nichts zurückgeben

114

Ich habe das useEffect-Beispiel wie folgt ausprobiert:

useEffect(async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}, []);

und ich bekomme diese Warnung in meiner Konsole. Aber die Bereinigung ist optional für asynchrone Anrufe, denke ich. Ich bin mir nicht sicher, warum ich diese Warnung bekomme. Sandbox zum Beispiel verknüpfen. https://codesandbox.io/s/24rj871r0p Geben Sie hier die Bildbeschreibung ein

RedPandaz
quelle

Antworten:

160

Ich schlage vor, die Antwort von Dan Abramov (einem der Hauptbetreuer von React) hier zu lesen :

Ich denke, Sie machen es komplizierter als es sein muss.

function Example() {
  const [data, dataSet] = useState<any>(null)

  useEffect(() => {
    async function fetchMyAPI() {
      let response = await fetch('api/data')
      response = await response.json()
      dataSet(response)
    }

    fetchMyAPI()
  }, [])

  return <div>{JSON.stringify(data)}</div>
}

Längerfristig werden wir von diesem Muster abraten, da es die Rennbedingungen fördert. Zum Beispiel könnte zwischen dem Beginn und dem Ende Ihres Anrufs alles passieren, und Sie hätten neue Requisiten bekommen können. Stattdessen empfehlen wir Suspense für das Abrufen von Daten, das eher so aussieht

const response = MyAPIResource.read();

und keine Effekte. In der Zwischenzeit können Sie das asynchrone Material in eine separate Funktion verschieben und aufrufen.

Mehr über experimentelle Spannung können Sie hier lesen .


Wenn Sie Funktionen außerhalb mit eslint verwenden möchten.

 function OutsideUsageExample() {
  const [data, dataSet] = useState<any>(null)

  const fetchMyAPI = useCallback(async () => {
    let response = await fetch('api/data')
    response = await response.json()
    dataSet(response)
  }, [])

  useEffect(() => {
    fetchMyAPI()
  }, [fetchMyAPI])

  return (
    <div>
      <div>data: {JSON.stringify(data)}</div>
      <div>
        <button onClick={fetchMyAPI}>manual fetch</button>
      </div>
    </div>
  )
}

Mit useCallback useCallback . Sandkasten .

import React, { useState, useEffect, useCallback } from "react";

export default function App() {
  const [counter, setCounter] = useState(1);

  // if counter is changed, than fn will be updated with new counter value
  const fn = useCallback(() => {
    setCounter(counter + 1);
  }, [counter]);

  // if counter is changed, than fn will not be updated and counter will be always 1 inside fn
  /*const fnBad = useCallback(() => {
      setCounter(counter + 1);
    }, []);*/

  // if fn or counter is changed, than useEffect will rerun
  useEffect(() => {
    if (!(counter % 2)) return; // this will stop the loop if counter is not even

    fn();
  }, [fn, counter]);

  // this will be infinite loop because fn is always changing with new counter value
  /*useEffect(() => {
    fn();
  }, [fn]);*/

  return (
    <div>
      <div>Counter is {counter}</div>
      <button onClick={fn}>add +1 count</button>
    </div>
  );
}
ZiiMakc
quelle
Sie können die Probleme mit den Rennbedingungen lösen, indem Sie überprüfen, ob die Komponente nicht wie folgt montiert ist: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Richard,
1
Sie können auch ein Paket mit dem Namen use-async-effect verwenden . Mit diesem Paket können Sie die asynchrone Wartesyntax verwenden.
KittyCat
Die Verwendung einer selbstaufrufenden Funktion, bei der kein Async in die useEffect-Funktionsdefinition gelangt, oder eine benutzerdefinierte Implementierung einer Funktion, die den Async-Aufruf als Wrapper um den useEffect auslöst, sind derzeit die beste Wahl. Sie können zwar ein neues Paket wie den vorgeschlagenen Use-Async-Effekt hinzufügen, aber ich denke, dies ist ein einfach zu lösendes Problem.
Thulani Chivandikwa
1
Hey, das ist in Ordnung und was ich die meiste Zeit mache. aber eslintbittet mich, fetchMyAPI()als Abhängigkeit vonuseEffect
Prakash Reddy Potlapadu
51

Wenn Sie eine asynchrone Funktion wie verwenden

async () => {
    try {
        const response = await fetch(`https://www.reddit.com/r/${subreddit}.json`);
        const json = await response.json();
        setPosts(json.data.children.map(it => it.data));
    } catch (e) {
        console.error(e);
    }
}

Es gibt ein Versprechen zurück und useEffecterwartet nicht, dass die Rückruffunktion Versprechen zurückgibt , sondern erwartet, dass nichts zurückgegeben wird oder eine Funktion zurückgegeben wird.

Als Problemumgehung für die Warnung können Sie eine selbstaufrufende asynchrone Funktion verwenden.

useEffect(() => {
    (async function() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    })();
}, []);

oder um es sauberer zu machen, können Sie eine Funktion definieren und sie dann aufrufen

useEffect(() => {
    async function fetchData() {
        try {
            const response = await fetch(
                `https://www.reddit.com/r/${subreddit}.json`
            );
            const json = await response.json();
            setPosts(json.data.children.map(it => it.data));
        } catch (e) {
            console.error(e);
        }
    };
    fetchData();
}, []);

Die zweite Lösung erleichtert das Lesen und hilft Ihnen beim Schreiben von Code zum Abbrechen früherer Anforderungen, wenn eine neue ausgelöst wird, oder zum Speichern der letzten Anforderungsantwort im Status

Arbeitscodesandbox

Shubham Khatri
quelle
Ein Paket, um dies zu vereinfachen, wurde erstellt. Sie finden es hier .
KittyCat
1
aber eslint wird das nicht tolerieren
Muhaimin CS
1
Es gibt keine Möglichkeit, Bereinigung / Didmount-Rückruf auszuführen
David Rearte
1
@ShubhamKhatri Wenn Sie verwenden, können useEffectSie eine Funktion zurückgeben, um die Bereinigung durchzuführen, z. B. das Abbestellen von Ereignissen. Wenn Sie die asynchrone Funktion verwenden, können Sie nichts zurückgeben, da useEffectdas Ergebnis nicht abgewartet wird
David Rearte
2
Wollen Sie damit sagen, dass ich eine Bereinigungsfunktion in eine asynchrone Funktion einfügen kann? Ich habe es versucht, aber meine Bereinigungsfunktion wird einfach nie aufgerufen. Können Sie ein kleines Beispiel geben?
David Rearte
32

Bis React einen besseren Weg bietet, können Sie einen Helfer erstellen useEffectAsync.js:

import { useEffect } from 'react';


export default function useEffectAsync(effect, inputs) {
    useEffect(() => {
        effect();
    }, inputs);
}

Jetzt können Sie eine asynchrone Funktion übergeben:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);
Ed I.
quelle
7
Der Grund, warum React nicht automatisch asynchrone Funktionen in useEffect zulässt, ist, dass in einem großen Teil der Fälle eine Bereinigung erforderlich ist. Die Funktion, useAsyncEffectwie Sie sie geschrieben haben, kann leicht dazu führen, dass jemand denkt, wenn er eine Bereinigungsfunktion von ihrem asynchronen Effekt zurückgibt, wird sie zum richtigen Zeitpunkt ausgeführt. Dies könnte zu Speicherverlusten oder schlimmeren Fehlern führen. Daher haben wir uns dafür entschieden, die Benutzer dazu zu ermutigen, ihren Code umzugestalten, um die „Naht“ von asynchronen Funktionen, die mit dem Lebenszyklus von React interagieren, sichtbarer und das Verhalten des Codes infolgedessen hoffentlich bewusster und korrekter zu machen.
Sophie Alpert
8

Ich habe diese Frage durchgelesen und bin der Meinung, dass der beste Weg zur Implementierung von useEffect in den Antworten nicht erwähnt wird. Angenommen, Sie haben einen Netzwerkanruf und möchten etwas tun, sobald Sie die Antwort erhalten haben. Speichern Sie der Einfachheit halber die Netzwerkantwort in einer Statusvariablen. Möglicherweise möchten Sie action / reducer verwenden, um den Speicher mit der Netzwerkantwort zu aktualisieren.

const [data, setData] = useState(null);

/* This would be called on initial page load */
useEffect(()=>{
    fetch(`https://www.reddit.com/r/${subreddit}.json`)
    .then(data => {
        setData(data);
    })
    .catch(err => {
        /* perform error handling if desired */
    });
}, [])

/* This would be called when store/state data is updated */
useEffect(()=>{
    if (data) {
        setPosts(data.children.map(it => {
            /* do what you want */
        }));
    }
}, [data]);

Referenz => https://reactjs.org/docs/hooks-effect.html#tip-optimizing-performance-by-skipping-effects

Chiranjib
quelle
1

Versuchen

const MyFunctionnalComponent: React.FC = props => {
  useEffect(() => {
    // Using an IIFE
    (async function anyNameFunction() {
      await loadContent();
    })();
  }, []);
  return <div></div>;
};

Nishla
quelle
1

Für andere Leser kann der Fehler darauf zurückzuführen sein, dass die asynchrone Funktion keine Klammern enthält:

Berücksichtigung der asynchronen Funktion initData

  async function initData() {
  }

Dieser Code führt zu Ihrem Fehler:

  useEffect(() => initData(), []);

Aber dieser wird nicht:

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

(Beachten Sie die Klammern um initData ()

Simon
quelle
1
Genial, Mann! Ich verwende Saga, und dieser Fehler trat auf, als ich einen Aktionsersteller aufrief, der das einzige Objekt zurückgab. Es sieht so aus, als ob die Rückruffunktion useEffect dieses Verhalten nicht leckt. Ich schätze deine Antwort.
Gorr1995
1
Nur für den Fall, dass sich die Leute fragen, warum dies wahr ist ... Ohne geschweifte Klammern wird der Rückgabewert von initData () implizit von der Pfeilfunktion zurückgegeben. Bei den geschweiften Klammern wird implizit nichts zurückgegeben, sodass der Fehler nicht auftritt.
Marnix.hoh
1

Hier kann der void-Operator verwendet werden.
Anstatt:

React.useEffect(() => {
    async function fetchData() {
    }
    fetchData();
}, []);

oder

React.useEffect(() => {
    (async function fetchData() {
    })()
}, []);

du könntest schreiben:

React.useEffect(() => {
    void async function fetchData() {
    }();
}, []);

Es ist etwas sauberer und hübscher.


Asynchrone Effekte können zu Speicherverlusten führen. Daher ist es wichtig, die Bereinigung der Komponente zu bereinigen. Im Falle eines Abrufs könnte dies folgendermaßen aussehen:

function App() {
    const [ data, setData ] = React.useState([]);

    React.useEffect(() => {
        const abortController = new AbortController();
        void async function fetchData() {
            try {
                const url = 'https://jsonplaceholder.typicode.com/todos/1';
                const response = await fetch(url, { signal: abortController.signal });
                setData(await response.json());
            } catch (error) {
                console.log('error', error);
            }
        }();
        return () => {
            abortController.abort(); // cancel pending fetch request on component unmount
        };
    }, []);

    return <pre>{JSON.stringify(data, null, 2)}</pre>;
}
kajkal
quelle