Avisos do React Hook para a função assíncrona em useEffect: a função useEffect deve retornar uma função de limpeza ou nada

117

Eu estava tentando o exemplo useEffect algo como abaixo:

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 recebo este aviso em meu console. Mas a limpeza é opcional para chamadas assíncronas, eu acho. Não sei por que recebo este aviso. Sandbox de vinculação para exemplos. https://codesandbox.io/s/24rj871r0p insira a descrição da imagem aqui

RedPandaz
fonte

Respostas:

164

Eu sugiro que você dê uma olhada na resposta de Dan Abramov (um dos principais mantenedores do React) aqui :

Acho que você está tornando tudo mais complicado do que o necessário.

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 longo prazo, desencorajaremos esse padrão porque incentiva as condições de corrida. Por exemplo, qualquer coisa pode acontecer entre o início e o fim da sua chamada e você pode ter obtido novos acessórios. Em vez disso, recomendaremos o Suspense para obtenção de dados, que se parecerá mais com

const response = MyAPIResource.read();

e sem efeitos. Mas enquanto isso, você pode mover o material assíncrono para uma função separada e chamá-lo.

Você pode ler mais sobre o suspense experimental aqui .


Se você quiser usar funções externas com o 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>
  )
}

Com 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
fonte
Você poderia resolver os problemas de condição de corrida verificando se o componente está desmontado da seguinte forma: useEffect(() => { let unmounted = false promise.then(res => { if (!unmounted) { setState(...) } }) return () => { unmounted = true } }, [])
Richard
1
Você também pode usar um pacote denominado use-async-effect . Este pacote permite que você use a sintaxe async await.
KittyCat
Usar uma função de auto-chamada não permite que o async vaze para a definição da função useEffect ou uma implementação customizada de uma função que acione a chamada async como um wrapper em torno do useEffect são a melhor aposta por agora. Embora você possa incluir um novo pacote como o efeito use-async sugerido, acho que este é um problema simples de resolver.
Thulani Chivandikwa
1
ei isso é bom e o que eu faço na maioria das vezes. mas eslintme pede para fazer fetchMyAPI()como dependência deuseEffect
Prakash Reddy Potlapadu
51

Quando você usa uma função assí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);
    }
}

ele retorna uma promessa e useEffectnão espera que a função de retorno de chamada retorne Promise, em vez disso, espera que nada seja retornado ou que uma função seja retornada.

Como uma solução alternativa para o aviso, você pode usar uma função assíncrona de chamada automática.

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 para torná-lo mais limpo, você pode definir uma função e chamá-la

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

a segunda solução tornará mais fácil de ler e ajudará você a escrever o código para cancelar as solicitações anteriores se uma nova for disparada ou salvar a última resposta da solicitação no estado

Códigosandbox de trabalho

Shubham Khatri
fonte
Um pacote para tornar isso mais fácil foi feito. Você pode encontrar aqui .
KittyCat
1
mas eslint não vai tolerar isso
Muhaimin CS
1
não há como executar callback de limpeza / desmontagem
David Rearte,
1
@ShubhamKhatri quando você usa useEffectvocê pode retornar uma função para fazer a limpeza, como cancelar a assinatura de eventos. Quando você usa a função assíncrona você não pode retornar nada porque useEffectnão vai esperar o resultado
David Rearte
2
você está dizendo que posso colocar uma função de limpeza em um assíncrono? Eu tentei, mas minha função de limpeza nunca é chamada. Você pode dar um pequeno exemplo?
David Rearte de
32

Até que o React forneça uma maneira melhor, você pode criar um auxiliar useEffectAsync.js:

import { useEffect } from 'react';


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

Agora você pode passar uma função assíncrona:

useEffectAsync(async () => {
    const items = await fetchSomeItems();
    console.log(items);
}, []);
Ed I
fonte
8
O motivo pelo qual o React não permite automaticamente funções assíncronas em useEffect é que, em uma grande parte dos casos, é necessária alguma limpeza. A função useAsyncEffectque você escreveu pode facilmente enganar alguém e pensar que se eles retornassem uma função de limpeza de seu efeito assíncrono, ela seria executada no momento apropriado. Isso poderia levar a vazamentos de memória ou erros piores, então optamos por encorajar as pessoas a refatorar seu código para tornar a "costura" das funções assíncronas que interagem com o ciclo de vida do React mais visível, e o comportamento do código como resultado, esperançosamente, mais deliberado e correto.
Sophie Alpert
8

Eu li esta questão e sinto que a melhor maneira de implementar useEffect não é mencionada nas respostas. Digamos que você tenha uma chamada de rede e gostaria de fazer algo assim que obtivesse a resposta. Para simplificar, vamos armazenar a resposta da rede em uma variável de estado. Pode-se querer usar ação / redutor para atualizar a loja com a resposta da rede.

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

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

Chiranjib
fonte
1

experimentar

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

Nishla
fonte
1

Para outros leitores, o erro pode vir do fato de que não há colchetes envolvendo a função assíncrona:

Considerando a função assíncrona initData

  async function initData() {
  }

Este código levará ao seu erro:

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

Mas este não vai:

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

(Observe os colchetes em torno de initData ()

Simon
fonte
1
Brilhante, cara! Estou usando o saga, e aquele erro apareceu quando eu estava chamando um criador de ação que retornou o único objeto. Parece que a função useEffect the callback não elimina esse comportamento. Agradeço sua resposta.
Gorr1995
2
Caso as pessoas estejam se perguntando por que isso é verdade ... Sem chaves, o valor de retorno de initData () é retornado implicitamente pela função de seta. Com as chaves, nada é retornado implicitamente e, portanto, o erro não acontecerá.
Marnix.hoh
1

O operador void pode ser usado aqui.
Ao invés de:

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

ou

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

você poderia escrever:

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

É um pouco mais limpo e bonito.


Os efeitos assíncronos podem causar vazamentos de memória, por isso é importante realizar uma limpeza na desmontagem do componente. Em caso de busca, poderia ser assim:

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
fonte