React Hook Warnings for async function in useEffect: la fonction useEffect doit retourner une fonction de nettoyage ou rien

119

J'essayais l'exemple useEffect quelque chose comme ci-dessous:

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

et j'obtiens cet avertissement dans ma console. Mais le nettoyage est facultatif pour les appels asynchrones, je pense. Je ne sais pas pourquoi je reçois cet avertissement. Lier le bac à sable pour des exemples. https://codesandbox.io/s/24rj871r0p entrez la description de l'image ici

RedPandaz
la source

Réponses:

166

Je suggère de regarder la réponse de Dan Abramov (l'un des principaux responsables de React) ici :

Je pense que vous rendez les choses plus compliquées que nécessaire.

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

À plus long terme, nous découragerons ce modèle car il encourage les conditions de course. Tels que - tout pourrait arriver entre le début et la fin de votre appel, et vous auriez pu obtenir de nouveaux accessoires. Au lieu de cela, nous vous recommandons Suspense pour la récupération de données qui ressemblera davantage à

const response = MyAPIResource.read();

et aucun effet. Mais en attendant, vous pouvez déplacer le contenu asynchrone vers une fonction distincte et l'appeler.

Vous pouvez en savoir plus sur le suspense expérimental ici .


Si vous souhaitez utiliser des fonctions à l'extérieur avec 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>
  )
}

Avec useCallback, useCallback . Bac à sable .

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
la source
Vous pouvez résoudre les problèmes de condition de concurrence en vérifiant si le composant est démonté comme suit: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Richard
1
Vous pouvez également utiliser un package nommé use-async-effect . Ce package vous permet d'utiliser la syntaxe d'attente async.
KittyCat
Utiliser une fonction auto-appelante pour ne pas laisser asynchrone fuir vers la définition de fonction useEffect ou une implémentation personnalisée d'une fonction qui déclenche l'appel async en tant que wrapper autour de useEffect est le meilleur choix pour le moment. Bien que vous puissiez inclure un nouveau package comme l'effet use-async suggéré, je pense que c'est un problème simple à résoudre.
Thulani Chivandikwa
1
hey c'est bien et ce que je fais la plupart du temps. mais eslintme demande de faire fetchMyAPI()comme dépendance deuseEffect
Prakash Reddy Potlapadu
51

Lorsque vous utilisez une fonction asynchrone comme

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

il renvoie une promesse et useEffect n'attend pas que la fonction de rappel renvoie Promise, mais s'attend plutôt à ce que rien ne soit retourné ou qu'une fonction soit retournée.

Pour contourner l'avertissement, vous pouvez utiliser une fonction asynchrone à invocation automatique.

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

ou pour le rendre plus propre, vous pouvez définir une fonction puis l'appeler

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 deuxième solution facilitera la lecture et vous aidera à écrire du code pour annuler les demandes précédentes si une nouvelle est déclenchée ou à enregistrer la dernière réponse à la demande dans l'état

Codes de travail et boîte

Shubham Khatri
la source
Un paquet pour faciliter cela a été fait. Vous pouvez le trouver ici .
KittyCat
1
mais eslint ne tolérera pas avec ça
Muhaimin CS
1
il n'y a aucun moyen d'exécuter le rappel de nettoyage / didmount
David Rearte
1
@ShubhamKhatri lorsque vous utilisez, useEffectvous pouvez renvoyer une fonction pour faire le nettoyage, comme se désabonner aux événements. Lorsque vous utilisez la fonction asynchrone, vous ne pouvez rien renvoyer car useEffectje n'attendrai pas le résultat
David Rearte
2
vous dites que je peux mettre une fonction de nettoyage dans une fonction asynchrone? J'ai essayé mais ma fonction de nettoyage n'est simplement jamais appelée. Pouvez-vous donner un petit exemple?
David Rearte
32

Jusqu'à ce que React offre une meilleure façon, vous pouvez créer une aide, useEffectAsync.js:

import { useEffect } from 'react';


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

Vous pouvez maintenant passer une fonction asynchrone:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);
Ed I
la source
8
La raison pour laquelle React n'autorise pas automatiquement les fonctions asynchrones dans useEffect est que dans une grande partie des cas, un nettoyage est nécessaire. La fonction useAsyncEffecttelle que vous l'avez écrite pourrait facilement induire quelqu'un en erreur en pensant que s'il retournait une fonction de nettoyage à partir de son effet asynchrone, elle serait exécutée au moment opportun. Cela pourrait conduire à des fuites de mémoire ou à des bogues pires, nous avons donc choisi d'encourager les gens à refactoriser leur code pour rendre la «couture» des fonctions asynchrones interagissant avec le cycle de vie de React plus visible, et le comportement du code en conséquence, espérons-le, plus délibéré et correct.
Sophie Alpert le
8

J'ai lu cette question et je pense que la meilleure façon d'implémenter useEffect n'est pas mentionnée dans les réponses. Disons que vous avez un appel réseau et que vous souhaitez faire quelque chose une fois que vous avez la réponse. Par souci de simplicité, stockons la réponse du réseau dans une variable d'état. On peut souhaiter utiliser action / réducteur pour mettre à jour le magasin avec la réponse du réseau.

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

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

Chiranjib
la source
2

L'opérateur void pourrait être utilisé ici.
Au lieu de:

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

ou

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

vous pourriez écrire:

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

C'est un peu plus propre et plus joli.


Les effets asynchrones peuvent provoquer des fuites de mémoire, il est donc important d'effectuer un nettoyage lors du démontage des composants. En cas de récupération, cela pourrait ressembler à ceci:

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
la source
1

essayer

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

Nishla
la source
1

Pour les autres lecteurs, l'erreur peut provenir du fait qu'il n'y a pas de parenthèses entourant la fonction asynchrone:

Considérant la fonction async initData

  async function initData() {
  }

Ce code entraînera votre erreur:

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

Mais celui-ci ne:

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

(Notez les crochets autour de initData ()

Simon
la source
1
Génial, mec! J'utilise saga, et cette erreur est apparue lorsque j'appelais un créateur d'action qui a renvoyé le seul objet. Il semble que la fonction useEffect de la fonction de rappel ne lèche pas ce comportement. J'apprécie votre réponse.
Gorr1995
2
Juste au cas où les gens se demanderaient pourquoi c'est vrai ... Sans accolades, la valeur de retour de initData () est implicitement retournée par la fonction de flèche. Avec les accolades, rien n'est retourné implicitement et l'erreur ne se produira donc pas.
Marnix.hoh le