¿Está bien usar async componentDidMount ()?

139

¿Es componentDidMount()una buena práctica usar como función asíncrona en React Native o debería evitarlo?

Necesito obtener información de AsyncStoragecuándo se monta el componente, pero la única forma que sé de hacerlo es hacer que la componentDidMount()función sea asíncrona.

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

¿Hay algún problema con eso y hay otras soluciones para este problema?

Mirakurun
fuente
1
La "buena práctica" es una cuestión de opinión. ¿Funciona? si.
Kraylog
2
Aquí hay un buen artículo que muestra por qué la espera asíncrona es una buena opción sobre las promesas hackernoon.com/…
Shubham Khatri
solo use redux-thunk para resolver el problema
Tilak Maddy
@TilakMaddy ¿Por qué supone que todas las aplicaciones de reacción usan redux?
Mirakurun
@Mirakurun, ¿por qué todo el desbordamiento de la pila supone que uso jQuery cuando solía hacer preguntas simples sobre JavaScript en el pasado?
Tilak Maddy

Respuestas:

162

Comencemos señalando las diferencias y determinando cómo podría causar problemas.

Aquí está el código del componentDidMount()método de ciclo de vida asíncrono y "sincronizado" :

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

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

Al mirar el código, puedo señalar las siguientes diferencias:

  1. Las asyncpalabras clave: en mecanografiado, esto es simplemente un marcador de código. Hace 2 cosas:
    • Forzar que el tipo de retorno sea en Promise<void>lugar de void. Si especifica explícitamente que el tipo de devolución no es prometedor (por ejemplo, nulo), el mecanografiado le arrojará un error.
    • Le permite usar awaitpalabras clave dentro del método.
  2. El tipo de retorno se cambia de voidaPromise<void>
    • Significa que ahora puedes hacer esto:
      async someMethod(): Promise<void> { await componentDidMount(); }
  3. Ahora puede usar la awaitpalabra clave dentro del método y pausar temporalmente su ejecución. Me gusta esto:

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

Ahora, ¿cómo podrían causar problemas?

  1. La asyncpalabra clave es absolutamente inofensiva.
  2. No puedo imaginar ninguna situación en la que necesite hacer una llamada al componentDidMount()método, por lo que el tipo de retorno también Promise<void>es inofensivo.

    Llamar a un método que tiene tipo de retorno de Promise<void>sin awaitpalabra clave no hará ninguna diferencia de llamar a uno que tiene tipo de retorno de void.

  3. Dado que no hay métodos de ciclo de vida después de componentDidMount()retrasar su ejecución, parece bastante seguro. Pero hay una trampa.

    Digamos que lo anterior this.setState({users, questions});se ejecutará después de 10 segundos. En medio del tiempo de retraso, otro ...

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

    ... se ejecutaron con éxito y el DOM se actualizó. El resultado fue visible para los usuarios. El reloj siguió marcando y transcurrieron 10 segundos. El retraso this.setState(...)se ejecutaría y el DOM se actualizaría nuevamente, esa vez con usuarios antiguos y preguntas antiguas. El resultado también sería visible para los usuarios.

=> Es bastante seguro (no estoy seguro del 100%) para usar asynccon el componentDidMount()método. Soy un gran admirador y hasta ahora no he encontrado ningún problema que me dé mucho dolor de cabeza.

Cù Đức Hiếu
fuente
Cuando habla sobre el problema en el que se produjo otro setState antes de una Promesa pendiente, ¿no es lo mismo con Promise sin el azúcar sintáctico asíncrono / en espera o incluso las devoluciones de llamada clásicas?
Clafou
3
¡Si! Retrasar un setState()siempre posee un pequeño riesgo. Debemos proceder con cuidado.
Cù Đức Hiếu
Supongo que una forma de evitar problemas es usar algo como isFetching: truedentro del estado de un componente. Solo he usado esto con redux, pero supongo que es completamente válido con la administración de estado de solo reacción. Aunque en realidad no resuelve el problema de que el mismo estado se actualice en otro lugar del código ...
Clafou
1
Estoy de acuerdo con eso. De hecho, la isFetchingsolución de bandera es bastante común, especialmente cuando queremos reproducir algunas animaciones en el front-end mientras esperamos la respuesta del back-end ( isFetching: true).
Cù Đức Hiếu
3
Puede tener problemas si establece setState después de desmontar el componente
Eliezer Steinbock
18

Actualización de abril de 2020: el problema parece haberse solucionado en la última Reacción 16.13.1, consulte este ejemplo de sandbox . Gracias a @abernier por señalar esto.


He investigado un poco y he encontrado una diferencia importante: React no procesa errores de los métodos asíncronos del ciclo de vida.

Entonces, si escribes algo como esto:

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

entonces su error será detectado por el límite de errores , y puede procesarlo y mostrar un mensaje elegante.

Si cambiamos el código así:

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

que es equivalente a esto:

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

entonces su error será tragado en silencio . La culpa es tuya, reacciona ...

Entonces, ¿cómo procesamos los errores que? La única forma parece ser una captura explícita como esta:

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

o así:

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

Si aún queremos que nuestro error alcance el límite de error, puedo pensar en el siguiente truco:

  1. Detecte el error, haga que el controlador de errores cambie el estado del componente
  2. Si el estado indica un error, tírelo desde el rendermétodo

Ejemplo:

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
fuente
¿Hay algún problema reportado para esto? Podría ser útil informarlo si aún es así ... gracias
abernier
@abernier Creo que es por dignidad ... Aunque probablemente podrían mejorarlo. No presenté ningún problema sobre esto ...
CF
1
Parece que ya no es el caso, al menos con React 16.13.1 como se prueba aquí: codesandbox.io/s/bold-ellis-n1cid?file=/src/App.js
abernier
9

Tu código está bien y es muy legible para mí. Vea este artículo de Dale Jefferson donde muestra un componentDidMountejemplo asíncrono y se ve muy bien también.

Pero algunas personas dirían que una persona que lee el código puede suponer que React hace algo con la promesa devuelta.

Entonces, la interpretación de este código y si es una buena práctica o no es muy personal.

Si quieres otra solución, puedes usar promesas . Por ejemplo:

componentDidMount() {
    fetch(this.getAuth())
      .then(auth => {
          if (auth) this.checkAuth(auth)
      })
}
Tiago Alves
fuente
3
... o también, simplemente use una asyncfunción en línea con awaits dentro ...?
Erik Kaplun
también una opción @ErikAllik :)
Tiago Alves
@ErikAllik, ¿tienes un ejemplo?
Pablo Rincón
1
@PabloRincon algo como (async () => { const data = await fetch('foo'); const result = await submitRequest({data}); console.log(result) })()dónde fetchy submitRequestson funciones que devuelven promesas.
Erik Kaplun
Este código es definitivamente malo, porque tragará cualquier error ocurrido en la función getAuth. Y si la función hace algo con la red (por ejemplo), se deben esperar errores.
CF
6

Cuando usa componentDidMountsin asyncpalabra clave, el documento dice esto:

Puede llamar a setState () inmediatamente en componentDidMount (). Activará una representación adicional, pero sucederá antes de que el navegador actualice la pantalla.

Si lo usa async componentDidMount, perderá esta capacidad: se producirá otro procesamiento DESPUÉS de que el navegador actualice la pantalla. Pero, si estás pensando en usar asíncrono, como buscar datos, no puedes evitar que el navegador actualice la pantalla dos veces. En otro mundo, no es posible PAUSAR componentDidMount antes de que el navegador actualice la pantalla

Lu Tran
fuente
1
Me gusta esta respuesta porque es concisa y está respaldada por documentos. ¿Puede agregar un enlace a los documentos a los que hace referencia?
theUtherSide
Esto incluso podría ser algo bueno, por ejemplo, si está mostrando un estado de carga mientras se carga el recurso y luego el contenido cuando se hace.
Hjulle
3

Actualizar:

(Mi versión: React 16, Webpack 4, Babel 7):

Al usar Babel 7 descubrirás:

Usando este patrón ...

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

te encontrarás con el siguiente error ...

Error de referencia no capturado: regeneratorRuntime no está definido

En este caso necesitará instalar babel-plugin-transform-runtime

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

Si por alguna razón no desea instalar el paquete anterior (babel-plugin-transform-runtime), entonces querrá apegarse al patrón Promise ...

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

Creo que está bien siempre y cuando sepas lo que estás haciendo. Pero puede ser confuso porque async componentDidMount()aún puede ejecutarse después de que se componentWillUnmounthaya ejecutado y el componente se haya desmontado.

También es posible que desee iniciar tareas sincrónicas y asincrónicas dentro componentDidMount. Si componentDidMountfuera asíncrono, tendría que poner todo el código síncrono antes del primero await. Puede que no sea obvio para alguien que el código anterior al primero se awaitejecuta sincrónicamente. En este caso, probablemente mantendría componentDidMountsincronizado, pero lo llamaría a métodos de sincronización y asíncrono.

Ya sea que elija async componentDidMount()los métodos de componentDidMount()llamada vs sincronización async, debe asegurarse de limpiar los oyentes o los métodos asíncronos que aún pueden estar ejecutándose cuando el componente se desmonta.

dosis
fuente
2

En realidad, la carga asíncrona en ComponentDidMount es un patrón de diseño recomendado a medida que React se aleja de los métodos de ciclo de vida heredados (componentWillMount, componentWillReceiveProps, componentWillUpdate) y pasa a Async Rendering.

Esta publicación de blog es muy útil para explicar por qué esto es seguro y para proporcionar ejemplos de carga asíncrona en ComponentDidMount:

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

DannyMoshe
fuente
3
La representación asíncrona en realidad no tiene nada que ver con hacer que el ciclo de vida sea explícitamente asíncrono. En realidad es un antipatrón. La solución recomendada es llamar a un método asíncrono desde un método de ciclo de vida
Clayton Ray el
1

Me gusta usar algo como esto

componentDidMount(){
   const result = makeResquest()
}
async makeRequest(){
   const res = await fetch(url);
   const data = await res.json();
   return data
}
Gustavo Miguel
fuente