Cómo evitar esta trampa de rendimiento de React Hooks

React Hooks promete evitar la sobrecarga de los componentes de la clase al tiempo que ofrece los mismos beneficios. Por ejemplo, nos permiten escribir componentes funcionales con estado sin tener que preocuparnos por almacenar el estado en la instancia de clase.

Sin embargo, escribir componentes con estado con Hooks requiere cuidado. Existe una sutil diferencia entre cómo se inicializa el estado en el constructor de un componente de clase y cómo se inicializa mediante el enlace useState. Los desarrolladores que ya entienden los componentes de clase y piensan en Hooks simplemente como componentes de clase sin las cosas de clase corren el riesgo de escribir componentes que rinden peor que los componentes de clase.

Aquí, analizo una característica de useState que solo se menciona brevemente en las preguntas frecuentes oficiales de Hooks. Comprender esta característica en detalle le permitirá aprovechar al máximo React Hooks. Además de leer esta nota, te invito a jugar con Stress Testing React Hooks, una herramienta de referencia que escribí para ilustrar estas peculiaridades de Hooks.

Las opciones anteriores a React Hooks

Suponga que tiene un cálculo costoso que debe realizarse solo una vez al configurar su componente, y suponga que este cálculo depende de algún accesorio. Un componente funcional simple hace un muy mal trabajo en esto:

Esto funciona muy mal, porque el cálculo costoso se realiza en cada render.

Los componentes de clase mejoran esto al permitirnos llevar a cabo una operación dada solo una vez, por ejemplo en el constructor:

Al almacenar el resultado del cálculo en la instancia, en este caso dentro del estado local del componente, podemos omitir el cálculo costoso en cada render posterior. Puede ver la diferencia que esto hace al comparar el componente de clase y el componente funcional con mi herramienta de referencia.

Pero los componentes de clase tienen sus propios inconvenientes, como se menciona en los documentos oficiales de React Hooks. Por eso se introdujeron los ganchos.

Una implementación ingenua con useState

El enlace useState se puede usar para declarar una "variable de estado" y establecerlo en un valor inicial. Ese valor se puede cambiar y acceder en representaciones posteriores. Con eso en mente, puede intentar ingenuamente hacer lo siguiente para mejorar el rendimiento de su componente funcional:

Puede pensar que, dado que aquí se trata de un estado que se comparte entre representaciones posteriores, el cálculo costoso solo se realiza en la primera representación, al igual que con los componentes de clase. Te equivocarías.

Para ver por qué, recuerde que NaiveHooksComponent es solo una función, una función que se invoca en cada render. Eso significa que useState se invoca en cada render. Cómo funciona useState es una historia complicada que no necesita preocuparnos. Lo importante es con qué se invoca useState: se invoca con el valor de retorno de costoso cálculo. Pero solo sabremos cuál es ese valor de retorno si en realidad invocamos el cálculo costoso. Como resultado, nuestro NaiveHooksComponent está condenado a realizar el costoso cálculo en cada render, al igual que nuestro FunctionalComponent anterior que no usaba useState.

Hasta ahora, useState no nos brinda ningún beneficio de rendimiento, como se puede verificar con mi herramienta de referencia. (Por supuesto, la matriz que usa el estado devuelve también contiene una función que nos permite actualizar fácilmente la variable de estado, que es algo que no podríamos hacer con un componente funcional simple).

Tres formas de memorizar cálculos caros

Afortunadamente, React Hooks nos proporciona tres opciones para manejar estados que son tan efectivos como los componentes de clase.

1. useMemo

La primera opción es usar el gancho useMemo:

Como regla general, useMemo solo realizará el costoso cálculo nuevamente si el valor de arg cambia. Sin embargo, esto es solo una regla general, ya que las versiones futuras de React pueden recalcular ocasionalmente el valor memorizado.

Las siguientes dos opciones son más confiables.

2. Pasando funciones a useState

La segunda opción es pasar una función para usar State:

Esta función solo se invoca en el primer render. Eso es super útil. (Aunque debe recordar que si desea almacenar una función real en estado, debe envolverla dentro de otra función. De lo contrario, terminará almacenando el valor de retorno de la función en lugar de la función misma).

3. useRef

La tercera opción es usar el gancho useRef:

Este es un poco extraño, pero funciona y está oficialmente sancionado. useRef devuelve un objeto de referencia mutable cuya clave actual apunta al argumento con el que se invoca useRef. Este objeto de referencia persistirá en representaciones posteriores. Entonces, si establecemos la corriente perezosamente como lo hicimos anteriormente, el cálculo costoso solo se realiza una vez.

Comparación

Como puede ver con mi herramienta de referencia, estas tres opciones son tan eficaces como nuestro componente de clase inicial. Sin embargo, el comportamiento de useMemo puede cambiar en el futuro. Entonces, si desea tener la garantía de que el cálculo costoso solo se realiza una vez, debe usar la opción 2, que pasa una función a useState, o la opción 3, que usa useRef.

La elección entre estas dos opciones se reduce a si alguna vez desea actualizar el resultado del cálculo costoso. Piense en la diferencia entre la opción 2 y la opción 3 como análoga a la diferencia entre almacenar algo en este estado o almacenarlo directamente en esto.