O uso do async componentDidMount () é bom?


139

O uso componentDidMount()como uma função assíncrona é uma boa prática no React Native ou devo evitá-lo?

Preciso obter algumas informações de AsyncStoragequando o componente é montado, mas a única maneira que sei tornar isso possível é tornar a componentDidMount()função assíncrona.

async componentDidMount() {
    let auth = await this.getAuth();
    if (auth) 
        this.checkAuth(auth);
}

Existe algum problema com isso e outras soluções para esse problema?


1
"Boas práticas" é uma questão de opinião. Funciona? sim.
— Kraylog

2
Aqui está um bom artigo que mostra por que a espera assíncrona é uma boa opção sobre as promessas hackernoon.com/…
— Shubham Khatri

basta usar redux-thunk ele resolverá o problema
— Tilak Maddy

@TilakMaddy Por que você supõe que todo aplicativo de reação usa redux?
— Mirakurun 22/07

@Mirakurun Por que todo o estouro de pilha supõe que eu uso o jQuery quando eu fazia perguntas simples sobre javascript no dia?
— Tilak Maddy 23/07

Respostas:


162

Vamos começar apontando as diferenças e determinando como isso pode causar problemas.

Aqui está o código do componentDidMount()método de ciclo de vida assíncrono e "sincronizado" :

// This is typescript code
componentDidMount(): void { /* do something */ }

async componentDidMount(): Promise<void> {
    /* do something */
    /* You can use "await" here */
}

Observando o código, posso apontar as seguintes diferenças:

  1. As asyncpalavras-chave: No texto datilografado, isso é apenas um marcador de código. Faz 2 coisas:
    • Force o tipo de retorno a ser em Promise<void>vez de void. Se você especificar explicitamente o tipo de retorno como não prometido (ex: void), o texto datilografado emitirá um erro em você.
    • Permite que você use awaitpalavras-chave dentro do método.
  2. O tipo de retorno é alterado de voidparaPromise<void>
    • Isso significa que agora você pode fazer isso:
      async someMethod(): Promise<void> { await componentDidMount(); }
  3. Agora você pode usar a awaitpalavra-chave dentro do método e pausar temporariamente sua execução. Como isso:

    async componentDidMount(): Promise<void> {
        const users = await axios.get<string>("http://localhost:9001/users");
        const questions = await axios.get<string>("http://localhost:9001/questions");
    
        // Sleep for 10 seconds
        await new Promise(resolve => { setTimeout(resolve, 10000); });
    
        // This line of code will be executed after 10+ seconds
        this.setState({users, questions});
        return Promise.resolve();
    }

Agora, como eles poderiam causar problemas?

  1. A asyncpalavra-chave é absolutamente inofensiva.
  2. Não consigo imaginar nenhuma situação em que você precise fazer uma chamada para o componentDidMount()método, para que o tipo de retorno Promise<void>também seja inofensivo.

    Chamar um método com o tipo de retorno Promise<void>sem awaitpalavra-chave não fará diferença em chamar um com o tipo de retorno void.

  3. Como não há métodos de ciclo de vida após componentDidMount()atrasar sua execução, parece bastante seguro. Mas há uma pegadinha.

    Digamos que o acima this.setState({users, questions});será executado após 10 segundos. No meio do tempo de atraso, outro ...

    this.setState({users: newerUsers, questions: newerQuestions});

    ... foram executados com sucesso e o DOM foi atualizado. O resultado foi visível para os usuários. O relógio continuou correndo e 10 segundos se passaram. O atraso this.setState(...)seria executado e o DOM seria atualizado novamente, dessa vez com usuários antigos e perguntas antigas. O resultado também seria visível para os usuários.

=> É bastante seguro (não tenho certeza de 100%) usar asynccom o componentDidMount()método Sou um grande fã e até agora não encontrei nenhum problema que me desse muita dor de cabeça.


Quando você fala sobre o problema em que outro setState ocorreu antes de uma promessa pendente, não é o mesmo com a Promise sem o açúcar sintático assíncrono / aguardado ou mesmo retornos de chamada clássicos?
— Clafou

3
Sim! Atrasar um setState()sempre possui um pequeno risco. Devemos proceder com cuidado.
— Cù Đức Hiếu

Eu acho que uma maneira de evitar problemas é usar algo como isFetching: truedentro do estado de um componente. Eu só usei isso com redux, mas suponho que seja completamente válido com o gerenciamento de estado somente de reação. Embora não resolve realmente o problema do mesmo estado que está sendo atualizado em outro lugar no código ...
— Clafou

1
Eu concordo com isso. De fato, a isFetchingsolução flag é bastante comum, especialmente quando queremos reproduzir algumas animações no front-end enquanto aguardamos pela resposta do back-end ( isFetching: true).
— Cù Đức Hiếu

3
Você pode ter problemas se você faz setState após o componente é desmontado
— Eliezer Steinbock

18

Atualização em abril de 2020: o problema parece ter sido corrigido no React 16.13.1 mais recente; veja este exemplo de sandbox . Obrigado a @abernier por apontar isso.


Fiz algumas pesquisas e descobri uma diferença importante: o React não processa erros dos métodos de ciclo de vida assíncronos.

Então, se você escrever algo como isto:

componentDidMount()
{
    throw new Error('I crashed!');
}

seu erro será detectado pelo limite do erro e você poderá processá-lo e exibir uma mensagem normal .

Se mudarmos o código assim:

async componentDidMount()
{
    throw new Error('I crashed!');
}

que é equivalente a isso:

componentDidMount()
{
    return Promise.reject(new Error('I crashed!'));
}

então o seu erro será silenciosamente engolido . Que vergonha, reagir ...

Então, como processamos erros? A única maneira parece ser uma captura explícita como esta:

async componentDidMount()
{
    try
    {
         await myAsyncFunction();
    }
    catch(error)
    {
        //...
    }
}

ou assim:

componentDidMount()
{
    myAsyncFunction()
    .catch(()=>
    {
        //...
    });
}

Se ainda queremos que nosso erro atinja o limite de erro, posso pensar no seguinte truque:

  1. Capture o erro, faça com que o manipulador de erros altere o estado do componente
  2. Se o estado indicar um erro, jogue-o no rendermétodo

Exemplo:

class BuggyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
  }

  buggyAsyncfunction(){ return Promise.reject(new Error('I crashed async!'));}

  async componentDidMount() {
    try
    {
      await this.buggyAsyncfunction();
    }
    catch(error)
    {
        this.setState({error: error});
    }
  }

  render() {
    if(this.state.error)
        throw this.state.error;

    return <h1>I am OK</h1>;
  }
}

há algum problema relatado para isso? Pode ser útil denunciá-lo se ainda for o caso ... thx
— abernier

@ abernier Eu acho que é por dignidade ... Embora provavelmente eles possam melhorar. Eu não arquivei nenhum problema sobre isso ...
— CF

1
parece não ser mais o caso, pelo menos com o React 16.13.1 como testado aqui: codesandbox.io/s/bold-ellis-n1cid?file=/src/App.js
— abernier

9

Seu código é bom e muito legível para mim. Veja este artigo de Dale Jefferson, onde ele mostra um componentDidMountexemplo assíncrono e parece muito bom também.

Mas algumas pessoas diriam que uma pessoa que lê o código pode assumir que o React faz algo com a promessa retornada.

Portanto, a interpretação deste código e se é uma boa prática ou não é muito pessoal.

Se você quiser outra solução, poderá usar promessas . Por exemplo:

componentDidMount() {
    fetch(this.getAuth())
      .then(auth => {
          if (auth) this.checkAuth(auth)
      })
}

3
... ou também, basta usar uma asyncfunção embutida com awaits dentro ...?
— Erik Kaplun

também uma @ErikAllik opção :)
— Tiago Alves

@ErikAllik, você tem um exemplo?
— Pablo Rincon

1
@PabloRincon parece (async () => { const data = await fetch('foo'); const result = await submitRequest({data}); console.log(result) })()onde fetche submitRequestsão funções que retornam promessas.
— Erik Kaplun

Esse código é definitivamente ruim porque engolirá qualquer erro ocorrido na função getAuth. E se a função fizer algo com a rede (por exemplo), erros deverão ser esperados.
— CF

6

Quando você usa componentDidMountsem asyncpalavra-chave, o documento diz o seguinte:

Você pode chamar setState () imediatamente em componentDidMount (). Isso acionará uma renderização extra, mas ocorrerá antes que o navegador atualize a tela.

Se você usar async componentDidMount, perderá essa capacidade: outra renderização APÓS o navegador atualizar a tela. Porém, se estiver pensando em usar assíncrono, como buscar dados, não é possível evitar que o navegador atualize a tela duas vezes. Em outro mundo, não é possível PAUSAR componentDidMount antes que o navegador atualize a tela


1
Eu gosto desta resposta porque é concisa e suportada por documentos. Você pode adicionar um link para os documentos que você está referenciando.
— theUtherSide

Isso pode até ser uma coisa boa, por exemplo, se você estiver exibindo um estado de carregamento enquanto o recurso estiver carregando e, em seguida, o conteúdo quando estiver pronto.
— Hjulle 26/03

3

Atualizar:

(Minha compilação: Reagir 16, Webpack 4, Babel 7):

Ao usar o Babel 7, você descobrirá:

Usando esse padrão ...

async componentDidMount() {
    try {
        const res = await fetch(config.discover.url);
        const data = await res.json();
        console.log(data);
    } catch(e) {
        console.error(e);
    }
}

você encontrará o seguinte erro ...

Não detectado ReferenceError: regeneratorRuntime não está definido

Nesse caso, você precisará instalar o babel-plugin-transform-runtime

https://babeljs.io/docs/en/babel-plugin-transform-runtime.html

Se, por algum motivo, você não desejar instalar o pacote acima (babel-plugin-transform-runtime), você deverá seguir o padrão Promise ...

componentDidMount() {
    fetch(config.discover.url)
    .then(res => res.json())
    .then(data => {
        console.log(data);
    })
    .catch(err => console.error(err));
}

3

Eu acho que está tudo bem, desde que você saiba o que está fazendo. Mas pode ser confuso porque async componentDidMount()ainda pode estar em execução após a componentWillUnmountexecução e o componente desmontado.

Você também pode iniciar tarefas síncronas e assíncronas componentDidMount. Se componentDidMountfosse assíncrono, você teria que colocar todo o código síncrono antes do primeiro await. Pode não ser óbvio para alguém que o código anterior ao primeiro awaité executado de forma síncrona. Nesse caso, eu provavelmente continuaria componentDidMountsíncrono, mas ele chamaria os métodos sync e async.

Independentemente de você escolher async componentDidMount()os métodos de componentDidMount()chamada de sincronização x async, é necessário limpar todos os ouvintes ou métodos assíncronos que ainda estejam em execução quando o componente for desmontado.


2

Na verdade, o carregamento assíncrono no ComponentDidMount é um padrão de design recomendado, pois o React se afasta dos métodos do ciclo de vida herdados (componentWillMount, componentWillReceiveProps, componentWillUpdate) e passa para a renderização assíncrona.

Esta postagem do blog é muito útil para explicar por que isso é seguro e fornecer exemplos para carregamento assíncrono no ComponentDidMount:

https://reactjs.org/blog/2018/03/27/update-on-async-rendering.html


3
A renderização assíncrona não tem nada a ver com tornar o ciclo de vida explicitamente assíncrono. Na verdade, é um anti-padrão. A solução recomendada é realmente chamar um método assíncrono de um método de ciclo de vida
— Clayton Ray

1

Eu gosto de usar algo assim

componentDidMount(){
   const result = makeResquest()
}
async makeRequest(){
   const res = await fetch(url);
   const data = await res.json();
   return data
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.