React Hook Avvisi per la funzione asincrona in useEffect: la funzione useEffect deve restituire una funzione di pulizia o niente


117

Stavo provando l'esempio useEffect qualcosa come di seguito:

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

e ottengo questo avviso nella mia console. Ma la pulizia è facoltativa per le chiamate asincrone credo. Non sono sicuro del motivo per cui ricevo questo avviso. Collegamento di sandbox per esempi. https://codesandbox.io/s/24rj871r0p inserisci qui la descrizione dell'immagine

Risposte:


164

Suggerisco di guardare la risposta di Dan Abramov (uno dei principali manutentori di React) qui :

Penso che tu lo stia rendendo più complicato di quanto dovrebbe essere.

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

A lungo termine scoraggeremo questo modello perché incoraggia le condizioni di gara. Ad esempio, potrebbe accadere qualsiasi cosa tra l'inizio e la fine della chiamata e potresti aver ottenuto nuovi oggetti di scena. Consigliamo invece Suspense per il recupero dei dati che avrà un aspetto più simile

const response = MyAPIResource.read();

e nessun effetto. Ma nel frattempo puoi spostare le cose asincrone in una funzione separata e chiamarla.

Puoi leggere di più sulla suspense sperimentale qui .


Se vuoi usare funzioni esterne con 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>
  )
}

Con useCallback useCallback . Sandbox .

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

Potresti risolvere i problemi delle condizioni di gara controllando se il componente è smontato in questo modo: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Richard

1
Puoi anche usare un pacchetto denominato use-async-effect . Questo pacchetto consente di utilizzare la sintassi di attesa asincrona.
KittyCat

L'uso di una funzione di auto invocazione non lascia che la perdita asincrona alla definizione della funzione useEffect o un'implementazione personalizzata di una funzione che inneschi la chiamata asincrona come wrapper attorno a useEffect è la soluzione migliore per ora. Sebbene sia possibile includere un nuovo pacchetto come l'effetto use-async suggerito, penso che questo sia un problema semplice da risolvere.
Thulani Chivandikwa

1
hey va bene e quello che faccio la maggior parte delle volte. ma eslintmi chiede di fare fetchMyAPI()come dipendenza diuseEffect
Prakash Reddy Potlapadu

51

Quando usi una funzione asincrona come

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

restituisce una promessa e useEffect non si aspetta che la funzione di callback restituisca Promessa, piuttosto si aspetta che non venga restituito nulla o che venga restituita una funzione.

Come soluzione alternativa per l'avviso, puoi utilizzare una funzione asincrona che richiama automaticamente.

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

oppure per renderlo più pulito si potrebbe definire una funzione e poi chiamarla

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

la seconda soluzione faciliterà la lettura e ti aiuterà a scrivere il codice per annullare le richieste precedenti se ne viene attivata una nuova o per salvare l'ultima richiesta di risposta nello stato

Codesandbox funzionante


È stato creato un pacchetto per rendere tutto più semplice. Puoi trovarlo qui .
KittyCat

1
ma eslint non lo tollera
Muhaimin CS

1
non è possibile eseguire la richiamata cleanup / didmount
David Rearte

1
@ShubhamKhatri quando lo usi useEffectpotresti restituire una funzione per pulire come annullare l'iscrizione agli eventi. Quando usi la funzione asincrona non puoi restituire nulla perché useEffectnon aspetterà il risultato
David Rearte

2
stai dicendo che posso mettere una funzione di pulizia in una asincrona? ho provato ma la mia funzione di pulizia non viene mai chiamata. Puoi fare un piccolo esempio?
David Rearte

32

Fino Reagire fornisce un modo migliore, è possibile creare un aiutante, useEffectAsync.js:

import { useEffect } from 'react';


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

Ora puoi passare una funzione asincrona:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);

8
Il motivo per cui React non consente automaticamente le funzioni asincrone in useEffect è che in un'enorme porzione di casi è necessaria una certa pulizia. La funzione useAsyncEffectcome l'hai scritta potrebbe facilmente indurre in errore qualcuno a pensare che se restituissero una funzione di pulizia dal loro effetto asincrono, sarebbe stata eseguita al momento opportuno. Ciò potrebbe portare a perdite di memoria o bug peggiori, quindi abbiamo deciso di incoraggiare le persone a rifattorizzare il loro codice per rendere più visibile la "cucitura" di funzioni asincrone che interagiscono con il ciclo di vita di React e, di conseguenza, il comportamento del codice, si spera, più deliberato e corretto.
Sophie Alpert

8

Ho letto questa domanda e sento che il modo migliore per implementare useEffect non è menzionato nelle risposte. Supponiamo che tu abbia una chiamata di rete e desideri fare qualcosa una volta che hai la risposta. Per semplicità, memorizziamo la risposta di rete in una variabile di stato. Si potrebbe voler utilizzare azione / riduttore per aggiornare il negozio con la risposta di rete.

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

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


1

provare

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


1

Per altri lettori, l'errore può derivare dal fatto che non ci sono parentesi che racchiudono la funzione asincrona:

Considerando la funzione async initData

  async function initData() {
  }

Questo codice porterà al tuo errore:

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

Ma questo non:

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

(Notare le parentesi attorno a initData ()


1
Fantastico, amico! Sto usando saga e quell'errore è apparso quando stavo chiamando un creatore di azioni che ha restituito l'unico oggetto. Sembra che useEffect la funzione di callback non lecchi questo comportamento. Apprezzo la tua risposta.
Gorr1995

2
Nel caso in cui le persone si chiedano perché questo è vero ... Senza parentesi graffe, il valore di ritorno di initData () viene restituito implicitamente dalla funzione freccia. Con le parentesi graffe, nulla viene restituito implicitamente e quindi l'errore non si verificherà.
Marnix.hoh

1

L'operatore void potrebbe essere utilizzato qui.
Invece di:

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

o

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

potresti scrivere:

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

È un po 'più pulito e carino.


Gli effetti asincroni potrebbero causare perdite di memoria, quindi è importante eseguire la pulizia durante lo smontaggio del componente. In caso di recupero, questo potrebbe essere simile a questo:

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>;
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.