React Hook Warnings para la función asincrónica en uso Efecto: la función useEffect debe devolver una función de limpieza o nada

120

Estaba probando el ejemplo useEffect algo como a continuación:

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

y recibo esta advertencia en mi consola. Pero la limpieza es opcional para las llamadas asíncronas, creo. No estoy seguro de por qué recibo esta advertencia. Vinculando sandbox para ejemplos. https://codesandbox.io/s/24rj871r0p ingrese la descripción de la imagen aquí

RedPandaz
fuente

Respuestas:

167

Sugiero ver la respuesta de Dan Abramov (uno de los mantenedores centrales de React) aquí :

Creo que lo estás complicando más de lo necesario.

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 largo plazo, desalentaremos este patrón porque fomenta las condiciones de carrera. Por ejemplo, cualquier cosa podría suceder entre el comienzo y el final de su llamada, y podría haber obtenido nuevos accesorios. En su lugar, recomendaremos Suspense para obtener datos que se parecerán más a

const response = MyAPIResource.read();

y sin efectos. Pero mientras tanto, puede mover las cosas asíncronas a una función separada y llamarla.

Puedes leer más sobre el suspenso experimental aquí .


Si desea utilizar funciones en el exterior 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>
  );
}
ZiiMakc
fuente
Puede resolver los problemas de condición de carrera comprobando si el componente está desmontado así: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Richard
1
También puede usar un paquete llamado use-async-effect . Este paquete le permite utilizar la sintaxis async await.
KittyCat
El uso de una función de autoinvocación no permite que async se filtre a la definición de la función useEffect o una implementación personalizada de una función que desencadena la llamada async como un envoltorio alrededor de useEffect son la mejor apuesta por ahora. Si bien puede incluir un nuevo paquete como el sugerido use-async-effect, creo que este es un problema simple de resolver.
Thulani Chivandikwa
1
oye eso está bien y lo que hago la mayoría de las veces. peroeslint me pide que haga fetchMyAPI()como dependencia deuseEffect
Prakash Reddy Potlapadu
51

Cuando usa una función asíncrona como

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

devuelve una promesa y useEffectno espera que la función de devolución de llamada devuelva Promise, sino que espera que no se devuelva nada o que se devuelva una función.

Como solución temporal a la advertencia, puede utilizar una función asíncrona autoinvocada.

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

o para hacerlo más limpio, puede definir una función y luego llamarla

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 segunda solución hará que sea más fácil de leer y lo ayudará a escribir código para cancelar solicitudes anteriores si se activa una nueva o guardar la última respuesta de solicitud en el estado

Códigos de trabajo y caja

Shubham Khatri
fuente
Se ha creado un paquete para facilitar esto. Puedes encontrarlo aquí .
KittyCat
1
pero eslint no tolerará eso
Muhaimin CS
1
no hay forma de ejecutar la devolución de llamada de limpieza / didmount
David Rearte
1
@ShubhamKhatri cuando lo usa useEffect, puede devolver una función para realizar la limpieza, como cancelar la suscripción a eventos. Cuando usa la función asíncrona, no puede devolver nada porque useEffectno esperará el resultado
David Rearte
2
¿Estás diciendo que puedo poner una función de limpieza en una asincrónica? Lo intenté, pero mi función de limpieza nunca se llama. ¿Puedes dar un pequeño ejemplo?
David Rearte
32

Hasta que React proporcione una forma mejor, puede crear un ayudante useEffectAsync.js:

import { useEffect } from 'react';


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

Ahora puede pasar una función asincrónica:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);
Ed I
fuente
9
La razón por la que React no permite automáticamente funciones asíncronas en useEffect es que en una gran parte de los casos, es necesario realizar una limpieza. La función useAsyncEffecttal como la ha escrito fácilmente podría inducir a error a alguien a pensar que si devuelve una función de limpieza de su efecto asincrónico, se ejecutaría en el momento adecuado. Esto podría provocar pérdidas de memoria o errores peores, por lo que optamos por alentar a las personas a refactorizar su código para hacer más visible la "costura" de las funciones asíncronas que interactúan con el ciclo de vida de React y, como resultado, el comportamiento del código, con suerte, más deliberado y correcto.
Sophie Alpert
8

Leí esta pregunta y siento que la mejor manera de implementar useEffect no se menciona en las respuestas. Supongamos que tiene una llamada de red y le gustaría hacer algo una vez que tenga la respuesta. En aras de la simplicidad, almacenemos la respuesta de la red en una variable de estado. Es posible que desee utilizar action / reducer para actualizar la tienda con la respuesta de la red.

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

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

Chiranjib
fuente
2

Aquí se podría utilizar el operador vacío .
En vez de:

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

o

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

podrías escribir:

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

Es un poco más limpio y bonito.


Los efectos asincrónicos pueden causar pérdidas de memoria, por lo que es importante realizar una limpieza al desmontar el componente. En caso de recuperación, esto podría verse así:

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
fuente
1

tratar

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

Nishla
fuente
1

Para otros lectores, el error puede provenir del hecho de que no hay corchetes que envuelvan la función asíncrona:

Considerando la función asíncrona initData

  async function initData() {
  }

Este código dará lugar a su error:

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

Pero este, no lo hará:

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

(Observe los corchetes alrededor de initData ()

Simón
fuente
1
¡Brillante, hombre! Estoy usando saga, y ese error apareció cuando estaba llamando a un creador de acciones que devolvió el único objeto. Parece que la función useEffect the callback no elimina este comportamiento. Agradezco tu respuesta.
Gorr1995
2
En caso de que la gente se pregunte por qué esto es cierto ... Sin llaves, el valor de retorno de initData () es devuelto implícitamente por la función de flecha. Con las llaves, no se devuelve nada implícitamente y, por lo tanto, no se producirá el error.
Marnix.hoh