Sergio Xalambrí

React v16.6: lazy, memo y más

Salió React v16.6, y con este vienen varias novedades, entre ellas el lanzamiento de la primera parte de React Suspense mediante una nueva función llamada lazy y de otra función para evitar doble renders llamada memo.

React.memo: Evitando doble renders

Esta función nos permite memoizar el render de un componente en base a sus props y evitar hacer otro render si estos no cambiaron. Esto ya era posible extendiendo de PureComponent, pero hacerlo así significaba crear sí o sí una clase con el consiguiente overhead al rendimiento y dificultar optimizaciones posibles sobre las funciones.

Esta nueva función entonces nos va a permitir memoizar un componente tanto creado como una clase como usando funciones. Incluso se puede hacer memoize del resultado de React.lazy.

import React, { memo } from "react";
import logo from './logo.svg';

function Logo({ alt }) {
  return <img src={logo} className="App-logo" alt={alt} />
}

export default memo(Logo);

Como vemos creamos el componente de forma normal y se lo pasamos a React.memo, este entonces devuelve el nuevo componente memoizado que podemos exportar.

Adicionalmente es posible pasar un segundo argumento a React.memo para personalizar la forma en que valida si cambiaron los props ya que por defecto hace un shallow equal de todos los props.

export default memo(Logo, (prevProps, nextProps) => {
  return prevProps.alt === nextProps.alt;
});

En este caso React.memo solo va a dejar que Logo se vuelva a renderizar si el prop alt cambió, pero si cualquier otro prop cambia se va a ignorar. Esto es similar a usar el método del ciclo de vida shouldComponentUpdate con una particularidad de que funciona a la inversa, se debe devolver true si el componente va a dar el mismo resultado y false si da diferente resultado, lo que significa que nuestra función no debe verificar si el componente debe actualizarse sino si los props son iguales.

Nota: La razón de llamarse memo es por memoize, se usa esta forma corta para evitar errores comunes al escribir la palabra memoize. No se llama pure como fue inicialmente ideado o como PureComponent debido a que no asegura la pureza del componente (que no tenga side effects), solamente que memoiza el resultado.

React.lazy: Code Split con Suspense

Esta nueva función, que se incorpora al core de React, permite hacer code split y lazy load de componente de React. Algo que hasta ahora era posible usando librerías como react-loadable o next/dynamic (de Next.js).

Esta función es simple de usar, recibe como único argumento una función asíncrona que devuelve una promesa de importar un componente de React. Dentro de esta función es posible agregar más lógica.

import { lazy } from "react";
import sleep from "sleep";

const Logo = lazy(async () => {
  await sleep(1000);
  return import("./logo.js");
});

En este caso el componente Logo que devuelve lazy va a esperar un segundo y recién entonces hacer import de nuestro componente ./logo.js. El sleep en este caso nos permite finjir una carga lenta para probar que efectivamente el componente esta siendo cargando de forma asíncrona.

El import funciona gracias al module bundler que uses, ya sea webpack, Parcel, Rollup o cualquier otro, estos van a crear un split point donde se use esta función y van a encargarse de que se cargue asíncronamente el módulo ./logo.js cuando se ejecute dicha función.

Nota: Este método todavía no funciona en el servidor, en futuras versiones va a estar disponible. Esto significa que no es posible usarlo con Next.js o SSR en general.

React.Suspense

Este componente esta relacionado con lazy y es obligatorio usarlo si usamos lazy load, en caso de no usarlo React muestra un error diciendo que es necesario.

Lo que hace Suspense es simple, envuelve nuestro componente lazy en otro componente y renderiza un fallback si es que todavía no se terminó de cargar el componente lazy.

import React, { Component, Suspense } from "react";
import LazyLogo from "./lazy-logo.js"; // nuestro componente lazy
import Placeholder from "./placeholder.js"; // un componente que sirva de placeholder

class App extends Component {
  state = {
    alt: "React"
  }

  render() {
    return (
      <Suspense maxDuration={300} fallback={<Placeholder />}>
        <LazyLogo alt={this.state.alt} />
      </Suspense>
    )
  }
}

Ahora cuando App sea renderizado este va a pasar su estado a LazyLogo y por consiguiente al componente de Logo, mientras el logo este siendo importado Suspense va a renderizar el componente Placeholder que pasamos con el prop fallback, este componente puede ser tanto algo genérico como un spinner o algo único para nuestro componente lazy.

Por último, el prop maxDuration nos permite indicar cuanto debe esperar Suspense antes de renderizar el fallback, esto nos sirve para que si un componente carga lo suficientemente rápido no rendericemos el fallback y evitemos que este se vea durante menos de un segundo.

Nota: el prop maxDuration solo funciona cuando el modo concurrente, antes llamado async, de React está habilitado, actualmente este modo no es estable por lo que maxDuration es simplemente ignorado.

static contextType: Accediendo al contexto más fácil

Con React 16.3 se introdujo el API estable para usar contexto, usando React.createContext.

import { createContext } from "react";
export default createContext();

Esta API, aunque práctica, solo permite usar el contexto en el método render de un componente. Lo que en componentes que son funciones no causa problemas, pero en clases que extienden Component o PureComponent impide su uso en el ciclo de vida.

Desde ahora hay otra forma de usar el contexto mediante static propTypes en una clase.

import { Component } from "react";

import MyContext from "./context.js"; // el archivo que creamos antes

class MyComponent extends React.Component {
  static contextType = MyContext;
  componentDidMount() {
    const value = this.context;
    // hacer algo con el contexto acá
  }
  componentDidUpdate() {
    const value = this.context;
    // hacer algo con el contexto
  }
  componentWillUnmount() {
    const value = this.context;
    // hacer algo con el contexto
  }
  render() {
    const value = this.context;
    // user el contexto para hacer render
  }
}

export default MyComponent;

Como vemos, con pasar el contexto que devuelve React.createContext es suficiente para empezar a usarlo en cualquier parte del ciclo de vida de un componente.

Nota: Usando este método solo es posible suscribirse a un contexto a la vez, para usar varios contextos se debería envolver a nuestro componente de clase en una función que use el API actual para pasar el contexto como prop.

static getDerivedStateFromError(): Reaccionando a los errores antes del render

En React v16 se introdujo también una forma de atrapar errores que ocurren en el render usando el método del ciclo de vida componentDidCatch. Este método se ejecuta cuando un render tira un errores y nos permite actualizar el estado reaccionar al error de alguna forma en nuestra UI.

Antes de que se cambie el estado React por defecto renderiza null, lo que en algunos casos puede romper al componente padre del que dio error si este no espera que falte alguna ref. Este método tampoco funciona al renderizar en el servidor ya que todos los métodos que se llaman Did se ejecutan solo en el navegador.

Desde ahora se puede usar el nuevo método estático getDerivedStateFromError() para obtener el error antes del render.

Nota: Este método todavía no funciona en el servidor, en futuras versiones va a estar disponible. Esto significa que no es posible usarlo con Next.js o SSR en general.

class ErrorBoundary extends React.Component {
  state = {
    hasError: false
  };

  static getDerivedStateFromError(error) {
    // retorna los nuevos cambios al estado
    return { hasError: true };
  }

  render() {
    if (this.state.hasError) {
      // Renderizamos algo en lugar del contenido si hay un error
      return <h1>Something went wrong.</h1>;
    }
    // renderizamos nuestro contenido
    return this.props.children; 
  }
}

El uso es igual que con componentDidCatch, se usa ErrorBoundary para envolver un componente y todos los errores que ocurran en sus componentes hijos serían atrapados por getDerivedStateFromError y nos permitiría reaccionar a este error.

Nuevas advertencias en StrictMode

En la versión 16.3 se introdujo un modo estricto a React que se puede usar envolviendo nuestra aplicación con el componente React.StrictMode.

Esta nueva versión incluye nuevas advertencias de funcionalides que se van a volver obsoletas en el futuro.

  • ReactDOM.findDOMNode(). Esta API se va a eliminar en el futuro, si nunca lo usaste se puede ignorar, si lo usaste hay una guía en la documentación explicando como actualizar.
  • Antíguo API de contexto usando contextTypes y getChildContext. El antíguo API de contexto vuelve a React más lento y pesado de lo que debería ser. La recomendación es actualizar al nuevo API para que en el futuro se pueda eliminar el soporte al API viejo.

Palabras finales

Como se ve esta nueva versión trae muchas cosas interesantes al ecosistema de React que en su mayoría se resolvían mediante librarías externas y ahora va a ser posible hacer solo con React.

Ya sea que evitemos renders innecesarios en un componente de función usando memo o carguemos de forma asíncrona usando lazy de a poco React nos va dando más y más herramientas para crear una mejor experiencia de usuario de forma más simple para los desarrolladores.

Por último si quieren ver como funciona lazy y memo pueden ver una demo en https://react-lazy-memo.now.sh y el código fuente en https://github.com/sergiodxa/react-lazy-memo.