L'uso del componente asincrono DidMount () è buono?


139

L'uso componentDidMount()come funzione asincrona è una buona pratica in React Native o dovrei evitarlo?

Devo ottenere alcune informazioni da AsyncStoragequando il componente si monta, ma l'unico modo che conosco per renderlo possibile è rendere componentDidMount()asincrona la funzione.

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

C'è qualche problema con questo e ci sono altre soluzioni a questo problema?


2
La "buona pratica" è una questione di opinione. Funziona? sì.
Kraylog,

2
Ecco un buon articolo che mostra perché async wait
Shubham Khatri,

basta usare redux-thunk per risolvere il problema
Tilak Maddy

@TilakMaddy Perché pensi che ogni app di reazione utilizzi redux?
Mirakurun,

@Mirakurun perché l'intero overflow dello stack ha presupposto che io usassi jQuery quando facevo domande semplici javascript nel corso della giornata?
Tilak Maddy,

Risposte:


162

Cominciamo sottolineando le differenze e determinando come potrebbe causare problemi.

Ecco il codice del componentDidMount()metodo del ciclo di vita asincrono e "sincronizza" :

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

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

Guardando il codice, posso sottolineare le seguenti differenze:

  1. Le asyncparole chiave: in dattiloscritto, questo è semplicemente un marcatore di codice. Fa 2 cose:
    • Forza il tipo restituito in modo che sia Promise<void>invece di void. Se specifichi esplicitamente che il tipo restituito è non promettente (es: void), dattiloscritto ti sputerà un errore.
    • Consentire di utilizzare awaitparole chiave all'interno del metodo.
  2. Il tipo restituito viene modificato da voidaPromise<void>
    • Significa che ora puoi farlo:
      async someMethod(): Promise<void> { await componentDidMount(); }
  3. Ora puoi utilizzare la awaitparola chiave all'interno del metodo e sospenderne temporaneamente l'esecuzione. Come questo:

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

Ora, come possono causare problemi?

  1. La asyncparola chiave è assolutamente innocua.
  2. Non riesco a immaginare alcuna situazione in cui è necessario effettuare una chiamata al componentDidMount()metodo, quindi anche il tipo restituito Promise<void>è innocuo.

    La chiamata a un metodo con tipo restituito Promise<void>senza awaitparola chiave non farà alcuna differenza rispetto alla chiamata con un tipo restituito di void.

  3. Dal momento che non ci sono metodi del ciclo di vita dopo aver componentDidMount()ritardato la sua esecuzione sembra abbastanza sicuro. Ma c'è un gotcha.

    Diciamo che quanto sopra this.setState({users, questions});verrebbe eseguito dopo 10 secondi. Nel mezzo del ritardo, un altro ...

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

    ... sono stati eseguiti con successo e il DOM è stato aggiornato. Il risultato era visibile agli utenti. L'orologio ha continuato a ticchettare e sono trascorsi 10 secondi. Il ritardo this.setState(...)verrebbe quindi eseguito e il DOM verrà nuovamente aggiornato, quella volta con vecchi utenti e vecchie domande. Il risultato sarebbe anche visibile agli utenti.

=> È abbastanza sicuro (non sono sicuro circa il 100%) da usare asynccon il componentDidMount()metodo. Ne sono un grande fan e finora non ho riscontrato problemi che mi causano troppo mal di testa.


Quando parli del problema in cui si è verificato un altro setState prima di una Promessa in sospeso, non è lo stesso con Promise senza lo zucchero sintetico asincrono / attendi o persino i callback classici?
Clafou,

3
Sì! Ritardare a setState()comporta sempre un piccolo rischio. Dovremmo procedere con cura.
Cù Đức Hiếu,

Immagino che un modo per evitare problemi sia usare qualcosa di simile isFetching: trueall'interno dello stato di un componente. L'ho usato solo con Redux, ma suppongo che sia completamente valido con la gestione dello stato di solo reazione. Anche se non risolve davvero il problema dello stesso stato che viene aggiornato da qualche altra parte nel codice ...
Clafou,

1
Sono d'accordo. In effetti, la isFetchingsoluzione flag è piuttosto comune soprattutto quando vogliamo riprodurre alcune animazioni in front-end in attesa di una risposta back-end ( isFetching: true).
Cù Đức Hiếu,

3
È possibile riscontrare problemi se si imposta setState dopo che il componente è stato smontato
Eliezer Steinbock

18

Aggiornamento aprile 2020: il problema sembra essere stato risolto nell'ultimo React 16.13.1, vedi questo esempio sandbox . Grazie a @abernier per averlo segnalato.


Ho fatto alcune ricerche e ho trovato un'importante differenza: React non elabora errori dai metodi del ciclo di vita asincrono.

Quindi, se scrivi qualcosa del genere:

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

quindi il tuo errore verrà colto dall'errore di lavanderia e potrai elaborarlo e visualizzare un messaggio grazioso.

Se cambiamo il codice in questo modo:

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

che equivale a questo:

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

quindi il tuo errore verrà ingerito silenziosamente . Vergogna, reagisci ...

Quindi, come possiamo elaborare gli errori di? L'unico modo sembra essere la cattura esplicita in questo modo:

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

o in questo modo:

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

Se vogliamo ancora che il nostro errore raggiunga il limite dell'errore, posso pensare al seguente trucco:

  1. Cattura l'errore, fai in modo che il gestore errori cambi lo stato del componente
  2. Se lo stato indica un errore, gettalo dal rendermetodo

Esempio:

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

c'è un problema segnalato per questo? Potrebbe essere utile segnalarlo se ancora fosse il caso ... grazie
abernier il

@abernier Penso che sia degnamente ... Anche se probabilmente potrebbero migliorarlo. Non ho presentato alcun problema al riguardo ...
CF

1
sembra non essere più il caso, almeno con React 16.13.1 come testato qui: codesandbox.io/s/bold-ellis-n1cid?file=/src/App.js
abernier

9

Il tuo codice è perfetto e molto leggibile per me. Vedi questo articolo di Dale Jefferson in cui mostra un componentDidMountesempio asincrono e ha anche un bell'aspetto.

Ma alcune persone direbbero che una persona che legge il codice potrebbe supporre che React faccia qualcosa con la promessa restituita.

Quindi l'interpretazione di questo codice e se è una buona pratica o no è molto personale.

Se vuoi un'altra soluzione, puoi usare le promesse . Per esempio:

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

3
... o anche, basta usare una asyncfunzione in linea con awaits dentro ...?
Erik Kaplun,

anche un'opzione @ErikAllik :)
Tiago Alves,

@ErikAllik ti capita di avere un esempio?
Pablo Rincon,

1
@PabloRincon smth piace (async () => { const data = await fetch('foo'); const result = await submitRequest({data}); console.log(result) })()dove fetche submitRequestsono funzioni che restituiscono promesse.
Erik Kaplun,

Questo codice è sicuramente errato, perché inghiottirà qualsiasi errore nella funzione getAuth. E se la funzione fa qualcosa con la rete (ad esempio), devono essere previsti errori.
CF

6

Quando si utilizza componentDidMountsenza asyncparole chiave, il documento dice questo:

È possibile chiamare setState () immediatamente in componentDidMount (). Attiverà un rendering extra, ma accadrà prima che il browser aggiorni lo schermo.

Se usi async componentDidMount perderai questa abilità: un altro rendering avverrà DOPO che il browser aggiorna lo schermo. Tuttavia, se stai pensando di utilizzare l'asincronizzazione, come il recupero dei dati, non puoi evitare che il browser aggiorni lo schermo due volte. In un altro mondo, non è possibile PAUSA componentDidMount prima che il browser aggiorni lo schermo


1
Mi piace questa risposta perché è concisa e supportata da documenti. Potete per favore aggiungere un link ai documenti a cui fate riferimento.
theUtherSide

Questo potrebbe anche essere una buona cosa, ad esempio se stai visualizzando uno stato di caricamento mentre la risorsa è in fase di caricamento e quindi i contenuti al termine.
Hjulle,

3

Aggiornare:

(La mia build: React 16, Webpack 4, Babel 7):

Quando usi Babel 7 scoprirai:

Utilizzando questo modello ...

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

ti imbatterai nel seguente errore ...

Errore di riferimento non rilevato: rigeneratorRuntime non è definito

In questo caso dovrai installare babel-plugin-transform-runtime

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

Se per qualche motivo non desideri installare il pacchetto sopra (babel-plugin-transform-runtime), ti consigliamo di aderire al modello Promise ...

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

3

Penso che vada bene finché sai cosa stai facendo. Ma può essere fonte di confusione perché async componentDidMount()può essere ancora in esecuzione dopo l' componentWillUnmountesecuzione e il componente è stato smontato.

Potresti anche voler avviare attività sincrone e asincrone all'interno componentDidMount. Se componentDidMountfosse asincrono, dovresti mettere tutto il codice sincrono prima del primo await. Potrebbe non essere ovvio a qualcuno che il codice prima del primo venga awaiteseguito in modo sincrono. In questo caso, probabilmente rimarrei componentDidMountsincrono, ma vorrei che chiamasse sync e metodi asincroni.

Sia che tu scelga i metodi di chiamata async componentDidMount()vs sincronizzazione , devi assicurarti di ripulire eventuali listener o metodi asincroni che potrebbero essere ancora in esecuzione quando il componente viene smontato.componentDidMount()async


2

In realtà, il caricamento asincrono in ComponentDidMount è un modello di progettazione consigliato poiché React si allontana dai metodi del ciclo di vita legacy (componentWillMount, componentWillReceiveProps, componentWillUpdate) e passa a Rendering asincrono.

Questo post sul blog è molto utile per spiegare perché questo è sicuro e fornire esempi per il caricamento asincrono in ComponentDidMount:

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


3
Il rendering asincrono in realtà non ha nulla a che fare con il rendere il ciclo di vita esplicitamente asincrono. In realtà è un anti-pattern. La soluzione raccomandata è in realtà chiamare un metodo asincrono da un metodo del ciclo di vita
Clayton Ray

1

Mi piace usare qualcosa del genere

componentDidMount(){
   const result = makeResquest()
}
async makeRequest(){
   const res = await fetch(url);
   const data = await res.json();
   return data
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.