React: Métodos del ciclo de vida de un componente

Antes de todo, estoy en el proceso de aprendizaje de React y en esta publicación voy a explicar, según mi entendimiento, los métodos del ciclo de vida de un componente con React.

A partir de la versión 16.3 de React se agregaron nuevos métodos del ciclo de vida de un componente para mejorar el rendimiento, buenas practicas y así obtener una mejor calidad de los componentes creados, principalmente en componentes con funcionalidad asíncrona, esto es muy importante porque normalmente el mundo real es asíncrono y los componentes que creamos son para ser utilizados por seres humanos.

Por esta razón también se empiezan a dejar de utilizar los siguientes métodos, esto sucederá a partir de la version 17 de React:

  • componentWillMount()
  • componentWillRecieveProps(nextProps)
  • componentWillUpdate(nextProps, nextState)

Dado que los anteriores métodos se dejaran de usar en la version 17, los siguientes métodos son los recomendados a utilizar al iniciar un nuevo proyecto:

Montado:

  • constructor()
  • static getDerivedStateFromProps(nextProps, prevState)
  • render()
  • componentDidMount()

Actualización:

  • static getDerivedStateFromProps(nextProps, prevState)
  • shouldComponentUpdate(nextProps, nextState)
  • getSnapshotBeforeUpdate(prevProps, prevState)
  • render()
  • componentDidUpdate()

Desmontado:

  • componentWillUnmount()

Si ordenamos los métodos de manera secuencial:

  • constructor()
  • static getDerivedStateFromProps(nextProps, prevState)
  • render()
  • shouldComponentUpdate(nextProps, nextState)
  • getSnapshotBeforeUpdate(prevProps, prevState)
  • render()
  • componentDidUpdate()
  • componentWillUnmount()

Y para visualizar su relación, aquí esta un diagrama de flujo:

Si observamos el diagrama, el método static getDerivedStateFromProps(nextProps, prevState) sustituye al método deprecado componentWillReceiveProps(nextProps), también parece ser que el método getSnapshotBeforeUpdate(prevProps, prevState) sustituye al método deprecado componentWillUpdate(nextProps, nextState).

Para fines de demostración vamos a crear un componente Padre y otro componente Animal, lo ideal es que el componente Padre maneje todo el state, pero para demostrar el funcionamiento de los métodos ocuparemos algo de state en nuestro componente Animal.

constructor(props)

El constructor es un método de la mayoría de los lenguajes de programación orientada a objetos, y se utiliza para crear la instancia de una clase, en nuestro caso para crear la instancia de nuestro componente, cabe mencionar que después de la ejecución de este método, nuestro componente aún no se pinta en nuestro navegador, al proceso de pintado, es decir, agregarlo al DOM de nuestra aplicación y renderizarlo, se le llama Montar o Mount en ingles.

Como buena practica de programación es importante ejecutar super() dentro de un constructor para que realice cualquier llamada a constructores padres, en el caso de react se debe llamar con las props recibidas en el constructor, o sea, super(props), esto nos permite poder acceder a las props a través de this.props dentro del constructor.

El constructor solo se usa para definir el estado de nuestro componente, el estado de nuestro componente en el constructor se define así:

constructor (props) {
    super(props);
    this.state = {
        propiedad: 'Algún valor'
    }
}

Si no defines ningún estado en el constructor, entonces no lo necesitas.

También si se te ocurre definir el state usando las props pasados como parámetros probablemente es mejor definir el state en un componente padre o en la raíz de todos los componentes porque el estado no estará sincronizado con los cambios de las propiedades.

constructor (props) {
    super(props);
    this.state = {
        propieda: props.nombrePropiedad
    }
}

También es útil para bindear el objeto this a los métodos que son utilizados en el método render() como parte de la vista, normalmente para manejadores de eventos.

class Padre extends React.Component {
    constructor (props) {
        super(props);
        this.state = { src: '' }
        this.cambiarAnimal = this.cambiarAnimal.bind(this);
    }
    
    cambiarAnimal () {
        this.setState({
            src: 'Algúna url que apunte a una imagen de un animal' 
        });
    }
    
    render() {
        return (
          <div>
            <Animal src={this.state.src}/>
            <button onClick={this.cambiarAnimal}>Cambiar animal</button>
          </div>
        );
    }
}

Aunque esto último se puede resolver con el uso de funciones flecha (arrow functions).

class Padre extends React.Component {
    constructor (props) {
        super(props);
        this.state = { src: '' }
    }
    
    cambiarAnimal = () => {
        this.setState({
            src: 'Algúna url que apunte a una imagen de un animal' 
        });
    }
    
    render() {
        return (
          <div>
            <Animal src={this.state.src}/>
            <button onClick={this.cambiarAnimal}>Cambiar animal</button>
          </div>
        );
    }
}

static getDerivedStateFromProps(nextProps, prevState)

Este método es statico, si, debe tener el modificador static que indica que este método no está enlazado a la instancia del componente, si no más bien a su clase. Se invoca después de instanciar un componente y también cuando el componente recibe cambios en las propiedades.

Debe tener siempre un valor de retorno, ya sea un objeto para actualizar el state o null si no se quiere actualizar el state en relación con los nuevos valores de las props recibidas. Es importante saber que este método se ejecuta también cuando un componente padre provoca que el componente hijo sea de nuevo renderizado, por esta razón debes comparar valores anteriores con los nuevos para evitar mandar a actualizar el state cuando no hubo realmente un cambio.

Podemos razonar que este método nos puede servir para mantener sincronizado nuestro state (o solo una parte) con las props pasadas desde un componente padre.

Por el momento en nuestro ejemplo del componente Animal solo visualizaremos los datos y regresaremos null porque no queremos actualizar el estado, además el atributo src de nuestra imagen se actualiza cuando la propiedad src del componente cambia.

...
static getDerivedStateFromProps (nextProps, prevState) {
    console.log('nextProps: ', nextProps);
    console.log('prevState: ', prevState);
    return null;
}

render() {
    return (
        <img className="cat__img"
          src={this.props.src}
          />
     );
}
...

Lo siguiente realmente no es necesario, pero para visualizar la ejecución de este método supongamos que dentro de nuestro componente Animal vamos a manejar la url(src) de la imagen del animal en this.state.src, así:

constructor (props) {
    this.state = {
        src: props.src
    };
}

static getDerivedStateFromProps (nextProps, prevState) {
    if (prevState.src !== nextProps.src) {
      // necesario para actualizar la imagen cada vez que cambie this.props.src
      return { src: nextProps.src };
    }
    
    return null;
}

render() {      
    return (
        <img className="cat__img"
          src={this.state.src}
          />
     );
}
...

Ahora prueba el código aquí, y revisa los mensajes de la consola, por el momento solo nos estamos enfocando en el constructor(props) y static getDerivedStateFromProps(nextProps, prevState):

render()

Este método es obligatorio en cualquier componente, pues como su nombre lo dice, se utiliza para obtener los elementos finales a visualizar o pintar en el navegador. Debe ser una función pura, es decir, no debe modificar las props, no debe modificar el state ni realizar operaciones del DOM.

Según mi entendimiento, el resultado de este método es utilizado por ReactDOM.render() para insertarlo en el DOM del navegador y si el componente en cuestión ha sido insertado previamente, solo se muta el DOM lo necesario para reflejar los nuevos cambios, esto quiere decir que render() regresa los objetos necesarios para que en otro lugar sean insertados en el DOM. Esto se puede comprobar si observas la consola del anterior ejemplo y luego das click sobre el botón “Cambiar animal”entonces veras que el método render() es ejecutado antes de getSnapshotBeforeUpdate() y componentDidUpdate(), con esto tengo una duda, ¿En que momento se modifica el DOM?, yo creo que se modifica el DOM con ReactDOM.render() antes de que componentDidUpdate() se ejecute, y siguiendo esta lógica, la primera vez que se inserta el componente en el DOM del navegador web sucede justo antes de componentDidMount() pero después de render().

...
render() {      
    return (
        <img className="cat__img"
          src={this.state.src}
          />
     );
}
...

componentDidMount()

Este método se ejecuta cuando nuestro componente está listo en el DOM, eso quiere decir que siguiendo el razonamiento explicado en el método render(), se ejecuta después de que React inserte o modifique el DOM con ReactDOM.render(), por eso es útil para realizar llamadas ajax, realizar operaciones con el DOM como agregar eventos y/o modificar elementos internos.

Dentro de este método es seguro cambiar el state, pero si ejecutamos this.setState() provocara que nuestro componente se vuelva a renderizar. La documentación oficial de React nos advierte tener cuidado con esto, pues puede causar problemas de rendimiento por renderizar nuestro componente varias veces, sin embargo es necesario para los casos de tomar medidas y posiciones de algunos elementos antes de renderizar, por ejemplo el tamaño y posición de modales y tooltips.

Para ver el uso de este método veamos el siguiente ejemplo, si revisamos la consola veremos que render() se ejecuta dos veces, también si damos click en el botón Cambiar animal, se nota que de nuevo render() se ejecuta dos veces. ¿Por qué sucede esto?, sucede porque dentro del método componentDidMount() agregamos un escuchador de eventos para la carga de la imagen del animal, al ejecutarse this.onImgLoad() dentro se ejecuta this.setState() y esta función provoca que el componente se vuelva a renderizar para mostrar las medidas exactas de la imagen cuando se termina de cargar.

shouldComponentUpdate(nextProps, nextState)

En versiones actuales de React, este método se ejecuta para decidir si los cambios en las props o el state merecen que se vuelva a renderizar el componente con estos nuevos datos.

El valor de retorno de esta función es true o false. Si el resultado de este método es true, los métodos render(), getSnapshotBeforeUpdate() y componentDidMount()no se ejecutan.

Recibe como parámetros las nuevos valores pros y del state, con estos valores y los valores actuales de nuestro componente podemos condicionar si es necesario volver a renderizar o no, de esta manera podemos mejorar el rendimiento manualmente.

Si no implementamos este método en nuestro componente, React toma como resultado el valor true, por lo que siempre se volverá a renderizar. El resultado no influye en los componentes hijos, si en un componente padre el resultado es false, esto no impide que componentes hijos necesiten volver a ser renderizados.

Es importante mencionar que en la documentación indica que tal vez en futuras versiones de React, este método no impida un renderizado del componente, o sea, que en un futuro si este método regresa falseaun así se ejecutaran los métodos render(), getSnapshotBeforeUpdate() y componentDidUpdate().

Esto último me deja con un sabor amargo, actualmente este método es un buen lugar para mejorar el rendimiento de nuestro componente porque evitamos el re-renderizado en situaciones que no sean necesarias, pero después en futuras versiones cabe la posibilidad de perder esta habilidad, entonces quiero pensar que deben existir otras maneras de mejorar este caso de rendimiento, ¿Alguien tiene alguna idea?.

Veamos un ejemplo, en el anterior método componentDidMount() mencionamos que agregamos un escuchador de eventos al finalizar la carga de la imagen para poder obtener sus medidas, estás medidas las mostramos en nuestro componente, pero esta el caso de dos imágenes de perros que tiene la misma medida, 128px - 128px, entonces el ancho y el alto de la imagen no cambia, por lo que no es necesario volver a renderizar nuestro componente.

Si cambiamos de entre el perro de raza chihuahua y el perro ladrando, podemos ver en la consola que el último método ejecutado es el shouldComponentUpdate(), como el resultado fue false, no se ejecutaron de nuevo los métodos render(), getSnapshotBeforeUpdate() y componentDidUpdate().

getSnapshotBeforeUpdate(prevProps, prevState)

Este método se ejecuta después de render() y antes de componentDidUpdate(), el valor que regresa la ejecución de este método se convierte en el tercer parámetro llamado snapshot de componentDidUpdate(prevProps, prevState, snapshot), este método debe regresar algún valor o null, si no es así, React nos advertirá con algo parecido al siguiente warning en la consola:

Warning:
Animal.getSnapshotBeforeUpdate(): A snapshot value (or null) must be returned. You have returned undefined.

Además recibe las props y el state antes de la actualización por lo que es fácil hacer comparaciones con las props y el state actuales a través de this.props y this.state.

Este método puede regresar cualquier tipo de valor, por ejemplo puede regresar datos del DOM como cuantos elementos existen en una determinada lista de elementos o la posición del scroll antes de que el componente sea actualizado a través de ReactDOM.render()(si nuestra hipótesis explicada en el método render() es correcta).

Si revisamos el código de getSnapshotBeforeUpdate() y componentDidUpdate() y además revisamos la consola, notaremos que getSnapshotBeforeUpdate() le envía a componentDidUpdate el numero de elementos del DOM que contiene el componente en cuestión, que son 2, el párrafo con las medidas de la imagen y la imagen del animal.

componentDidUpdate(prevProps, prevState, snapshot)

Este método se ejecuta cuando el componente ha sido actualizado totalmente, y esta es reflejado en el DOM de nuestra aplicación, recibe las props y el state antes de la actualización por lo que es fácil hacer comparaciones con las props y el state actuales a través de this.props y this.state.

Aquí se puede trabajar con el DOM del componente dado que este mismo ha sido actualizado, además se puede realizar operaciones como obtener datos remotos según los cambios de las props o state.

No se ejecuta la primera vez que se usa el método render(), es decir, cuando se monta el componente.

En la consola podemos ver los datos, prevProps, preState, y snapshot, este último tiene el valor 2. También vemos this.props y this.state

componentWillUnmount()

Este método se ejecuta justo antes del que el componente sea desmontado del DOM, es un buen lugar para liberar recursos y memoria. Un ejemplo claro es la eliminación de escuchadores de eventos que ya no se van a necesitar, también se pueden cancelar peticiones remotas que se estén ejecutando actualmente dado que estas seguirán su proceso aún desmontando el componente.

En el ejemplo de abajo se ilustra como se elimina un escuchador de evento load para la carga de la imagen del animal.

1 Comment

  1. Gracias por el articulo, de verdad que no encontrabaun ejemplo claro con getDerivedStateFromProps,gracias por compartir tu experiencia. Se te agradece mucho.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

This site uses Akismet to reduce spam. Learn how your comment data is processed.