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?

Mirakurun
fonte
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.

Cù ếc Hiếu
fonte
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>;
  }
}
CF
fonte
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)
      })
}
Tiago Alves
fonte
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

Lu Tran
fonte
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));
}
Chade
fonte
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.

dosentmatter
fonte
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

DannyMoshe
fonte
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
}
Gustavo Miguel
fonte