React Hook Warnings dla funkcji async in useEffect: funkcja useEffect musi zwracać funkcję porządkującą lub nic

141

Próbowałem na useEffectprzykładzie jak poniżej:

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

i dostaję to ostrzeżenie na mojej konsoli. Myślę, że czyszczenie jest opcjonalne dla wywołań asynchronicznych. Nie wiem, dlaczego otrzymuję to ostrzeżenie. Podłączanie piaskownicy do przykładów. https://codesandbox.io/s/24rj871r0p wprowadź opis obrazu tutaj

RedPandaz
źródło

Odpowiedzi:

195

Proponuję spojrzeć na odpowiedź Dana Abramova (jednego z głównych opiekunów Reacta) :

Myślę, że komplikujesz to bardziej, niż powinno.

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

Na dłuższą metę zniechęcamy do tego wzoru, ponieważ sprzyja on warunkom wyścigu. Na przykład - wszystko może się zdarzyć między rozpoczęciem i zakończeniem rozmowy, a Ty mogłeś zdobyć nowe rekwizyty. Zamiast tego zalecamy Suspense do pobierania danych, które będzie wyglądać bardziej jak

const response = MyAPIResource.read();

i żadnych efektów. Ale w międzyczasie możesz przenieść elementy asynchroniczne do oddzielnej funkcji i wywołać ją.

Możesz przeczytać więcej o eksperymentalnym napięciu tutaj .


Jeśli chcesz używać funkcji na zewnątrz z eslint.

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

Z useCallback useCallback . Piaskownica .

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
źródło
Możesz rozwiązać problemy z warunkami wyścigu, sprawdzając, czy komponent nie jest odmontowany w następujący sposób: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Richard
1
Możesz także użyć pakietu o nazwie use-async-effect . Ten pakiet umożliwia użycie składni async await.
KittyCat
Użycie funkcji auto-wywołującej, która nie pozwala na przeciek asynchroniczny do definicji funkcji useEffect lub niestandardowej implementacji funkcji, która wyzwala wywołanie asynchroniczne jako opakowanie wokół useEffect, jest obecnie najlepszym rozwiązaniem. Chociaż możesz dołączyć nowy pakiet, taki jak sugerowany efekt użycia asynchronicznego, myślę, że jest to prosty problem do rozwiązania.
Thulani Chivandikwa
2
hej, to jest w porządku i co robię najczęściej. ale eslintprosi mnie o fetchMyAPI()uzależnienieuseEffect
Prakash Reddy Potlapadu
52

Gdy używasz funkcji asynchronicznej, takiej jak

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

zwraca obietnicę i useEffectnie oczekuje, że funkcja zwrotna zwróci Promise, raczej oczekuje, że nic nie zostanie zwrócone lub funkcja zostanie zwrócona.

Aby obejść ostrzeżenie, możesz użyć samowywołującej się funkcji asynchronicznej.

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

lub aby uczynić ją bardziej przejrzystą, możesz zdefiniować funkcję, a następnie ją wywołać

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

drugie rozwiązanie ułatwi odczyt i pomoże napisać kod, który anuluje poprzednie żądania, jeśli zostanie uruchomione nowe lub zapisze ostatnią odpowiedź na żądanie w stanie

Działające kodyandbox

Shubham Khatri
źródło
Pakiet, który ma to ułatwić. Znajdziesz go tutaj .
KittyCat
1
ale eslint nie będzie tego tolerował
Muhaimin CS
1
nie ma możliwości wykonania wywołania zwrotnego cleanup / didmount
David Rearte,
1
@ShubhamKhatri, gdy używasz, useEffectmożesz zwrócić funkcję do czyszczenia, taką jak anulowanie subskrypcji wydarzeń. Podczas korzystania z funkcji asynchroniczny nie można niczego powrócić, ponieważ useEffectnie będzie czekać na wynik
David Rearte
2
mówisz, że mogę umieścić funkcję porządkującą w asynchronicznej? Próbowałem, ale moja funkcja czyszczenia po prostu nie jest wywoływana. Czy możesz podać mały przykład?
David Rearte,
33

Dopóki React nie zapewni lepszego sposobu, możesz stworzyć pomocnika useEffectAsync.js:

import { useEffect } from 'react';


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

Teraz możesz przekazać funkcję asynchroniczną:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);
Ed I
źródło
9
Powodem, dla którego React nie zezwala automatycznie na korzystanie z funkcji asynchronicznych w useEffect, jest to, że w dużej części przypadków konieczne jest pewne czyszczenie. Funkcja useAsyncEffect, jaką napisałeś, może łatwo zmylić kogoś, kto by pomyślał, że jeśli zwróci funkcję porządkującą z efektu asynchronicznego, zostanie ona uruchomiona w odpowiednim czasie. Może to prowadzić do wycieków pamięci lub gorszych błędów, więc zdecydowaliśmy się zachęcić ludzi do refaktoryzacji ich kodu, aby „szew” funkcji asynchronicznych wchodzących w interakcje z cyklem życia Reacta był bardziej widoczny, a zachowanie kodu w rezultacie, miejmy nadzieję, było bardziej przemyślane i poprawne.
Sophie Alpert
8

Przeczytałem to pytanie i czuję, że najlepszy sposób na wdrożenie useEffect nie jest wymieniony w odpowiedziach. Załóżmy, że masz połączenie sieciowe i chcesz coś zrobić, gdy otrzymasz odpowiedź. Dla uproszczenia zapiszmy odpowiedź sieci w zmiennej stanu. Można chcieć użyć akcji / reduktora, aby zaktualizować sklep z odpowiedzią sieciową.

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

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

Chiranjib
źródło
4

W przypadku innych czytelników błąd może wynikać z faktu, że nie ma nawiasów zawijających funkcję async:

Biorąc pod uwagę funkcję asynchroniczną initData

  async function initData() {
  }

Ten kod doprowadzi do błędu:

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

Ale ten nie:

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

(Zwróć uwagę na nawiasy wokół initData ()

Szymon
źródło
1
Świetnie, stary! Używam sagi i ten błąd pojawił się, gdy dzwoniłem do kreatora akcji, który zwrócił jedyny obiekt. Wygląda na to, że funkcja useEffect nie liże tego zachowania. Doceniam twoją odpowiedź.
Gorr1995
2
Na wypadek gdyby ludzie zastanawiali się, dlaczego to prawda ... Bez nawiasów klamrowych, wartość zwracana przez initData () jest niejawnie zwracana przez funkcję strzałki. W przypadku nawiasów klamrowych nic nie jest zwracane niejawnie, a zatem błąd nie wystąpi.
Marnix.hoh
4

Można tu użyć operatora void .
Zamiast:

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

lub

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

mógłbyś napisać:

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

Jest trochę czystszy i ładniejszy.


Efekty asynchroniczne mogą powodować wycieki pamięci, dlatego ważne jest, aby wykonać czyszczenie po odmontowaniu komponentu. W przypadku pobierania może to wyglądać tak:

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

  React.useEffect(() => {
    const abortController = new AbortController()
    ;(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
źródło
Znasz swojego JS, sir. AbortController to nowa fangled rzecz, która jest dostępna po niepowodzeniu propozycji anulowania obietnic
Devin G Rhode
A tak przy okazji, istnieje pakiet "użyj-abortable-fetch", ale nie jestem pewien, czy podoba mi się, jak został napisany. Byłoby miło uzyskać prostą wersję tego kodu, który masz tutaj jako niestandardowy punkt zaczepienia. Ponadto „await-here” to całkiem niezły pakiet, który może zmniejszyć potrzebę stosowania bloku try / catch.
Devin G Rhode
1

próbować

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

Nishla
źródło