La incorporación de los hooks en React nos permite desarrollar componentes funcionales con estado y ciclo de vida y, por lo tanto, prescindir de los componentes de clase.

En Octubre de 2018, en la React Conf, el equipo de Facebook encargado del desarrollo del proyecto React (en ese momento Sophie Alpert y Dan Abramov) presentó a la comunidad una nueva propuesta: los Hooks. En Febrero de 2019, la versión 16.8 de React fue liberada al público y con ella esta nueva característica que introdujo uno de los cambios más interesantes en esta librería de desarrollo de interfaces de usuario. En React Native, los hooks fueron incorporados a partir de la versión 0.59.

¿Qué problemas tenía React y vienen a solucionar los Hooks?

Tras cinco años de existencia de la librería y gracias a la experiencia acumulada en el desarrollo de componentes, el equipo de React había logrado identificar tres problemas que debía corregir:

En primer lugar, la posibilidad de reusar lógica entre componentes. Los dos patrones principales para compartir código entre componentes, hasta ese momento, eran los High-Order Components y las Render Props. Si bien son formas correctas para lograr este objetivo, en algunos casos complejos esto podría derivar en un árbol de componentes anidados tan extenso que se conoce como Wrapper Hell.

Otro problema habitual es el desarrollo de componentes complejos y extensos. En ocasiones, existe la necesidad de tener que dividir la lógica de una misma funcionalidad utilizando los métodos componentDidMount, componentDidUpdate, componentWillUnmount. Por ejemplo, si tenemos que agregar eventListeners en componentDidMount y limpiarlos en componentWillUnmount, estamos separando lógica relacionada, por la necesidad de tener que realizar estas acciones en función del ciclo de vida del componente.

Finalmente, las clases de Javascript suponen una dificultad no sólo para desarrolladores, sino para máquinas también. Por poner un ejemplo, es común la dificultad para entender la utilización de binding para no perder el contexto cuando queremos referenciar un método con la palabra reservada this. En el caso de las máquinas también se identificaron dificultades para implementar hot-reloading de manera fiable por el uso de clases, y algunos problemas para mejorar la performance de los componentes debido a patrones que dificultan la optimización en el momento de la compilación.

La solución: encontrar una alternativa a los componentes de clase

Si bien existía la posibilidad de intentar solucionar estos problemas de manera individual, es probable que la solución de uno de ellos implicase el agravamiento de los otros dos. El equipo de React entendió que estos no eran tres problemas aislados, sino tres síntomas de un mismo problema: la falta de un componente más simple que las clases, capaz de tener estado y ciclo de vida.

Para ello surgen los hooks, que son funciones que permiten enganchar nuestros componentes funcionales a características propias de un componente de clase. Es decir, proporcionan un estado y un ciclo de vida a estos componentes evitándonos a los desarrolladores el uso de las clases.

El hook de estado: useState

El hook useState es el que nos permite agregarle un estado local a un componente funcional y cambiar ese estado.

Veamos un ejemplo:

import React, { useState } from 'react';
 
function ShowTextExample() {
 const [showText, setShowText] = useState(false);
 
 return (
   <div>
     <button
       type="button"
       onClick={() => setShowText(true)}
     >
       Mostrar Texto
     </button>
     {
       showText && ¡Se muestra el texto!
     }
   </div>
 );
}
 
export default ShowTextExample;

El componente ShowTextExample tiene un botón (Mostrar Texto) y una etiqueta <h1>¡Se muestra el Texto</h1>, que depende del valor de la variable showText para ser visible. Hasta aquí nada nuevo si ya has desarrollado con React; se trata de un renderizado condicional, algo muy habitual.

Sin embargo, en la primera parte del componente, hemos utilizado el hook useState. En primer lugar, declaramos dos variables utilizando array destructuring: la primera, showText, representa el estado del componente; la segunda, setShowText, representa la función con la que cambiaremos el valor de ese estado (lo que en un componente de clase haríamos con this.setState({...}). Además, vemos que le asignamos un valor inicial (false).

Finalmente, utilizamos un manejador para el evento onClick del botón, setShowText, para cambiar el estado pasándole el valor true, lo que permite que se muestre el texto.

El hook de efecto: useEffect

Este hook es el que utilizaremos para reemplazar los métodos del ciclo de vida de los componentes de clase: ComponentDidMount, ComponentDidUpdate, ComponentWillUnmount. El hook useEffect es el equivalente a estos tres métodos combinados. Este hook se ejecuta siempre después del primer renderizado y después de cada actualización y, por lo tanto, se utiliza para ejecutar funciones después de hacer render. useEffect recibe una función que puede realizar todo tipo de operación incluyendo efectos secundarios. Un ejemplo típico de una operación que podemos querer realizar es una llamada a un servicio.

Una pregunta lógica que puede surgir en este punto es: ¿si _useEffec_t se ejecuta en cada renderizado cómo afecta esto al funcionamiento? ¿Cómo se puede evitar que entre en bucle?

Siguiendo con el ejemplo de una petición, en los componentes de clase solemos controlar estas llamadas con una comparación en el método ComponentDidUpdate, para asegurarnos de que no se haga la petición innecesariamente si, por ejemplo, un valor se mantuvo equivalente entre renderizados. Con useEffect también podemos lograr esto, pasándole a la función, un array como segundo parámetro. El valor de este array será el que React comparará para saber si tiene que volver a ejecutar el hook. En caso de no variar este valor, el hook no se ejecutará. Veamos un ejemplo:

import React, { useState, useEffect } from 'react';
 
function FormExample() {
 const [name, setName] = useState('');
 const [lastName, setLastName] = useState('');
 useEffect(() => {
   console.log('se ha ejecutado el hook');
 }, [name]);
 
 return (
   <div>
     <input value={name} onChange={(event) => setName(event.target.value)} />
     <input value={lastName} onChange={(event) => setLastName(event.target.value)} />
   </div>
 );
}

En este caso tenemos dos inputs: uno para establecer la variable name y el otro para lastName. El hook useEffect simplemente realiza un console.log() en cada ocasión en que se ejecuta. Si no le pasásemos el segundo parámetro [name], veríamos el resultado de console.log() cada vez que el usuario introduce un valor en cualquiera de los dos inputs. En cambio, al pasarle la variable name como dependencia, el hook se ejecuta solamente cuando el usuario introduce un valor en el input correspondiente a name.

Un error que cometí, la primera vez que utilicé useEffect para lanzar una petición, fue no pasarle este segundo parámetro, lo cual provocó que el componente se actualizara con el resultado de la llamada y volviera a ejecutarse en un loop infinito. Una forma de evitar esto es pasarle un array vacío como segundo parámetro y así el hook se ejecutará una única vez. Esto es lo que hacemos en el siguiente ejemplo:

function FavArticles(props, context) {
 
const [articles, setArticles] = useState([]);
 useEffect(() => {
   fetch(apiRoutes.favourites)
   .then(res => res.json())
   .then(response => setArticles(response.articles));
 }, [])
 
 
 return (

El hook de contexto: useContext

Para explicar la utilidad del hook useContext, primero debemos mencionar el funcionamiento de la API Context de React, liberada de forma no experimental a partir de la versión 16.3.

Esta API nos permite acceder a datos globales de nuestra aplicación desde cualquier componente sin necesidad de pasar esta información manualmente mediante props, a través de todos los niveles de componentes hasta llegar al que realmente lo necesita. Algunos ejemplos de datos típicos que se pueden considerar globales son el tema de la interfaz o los datos del usuario logueado.

La API de Context contiene un componente Provider y un componente Consumer. El primero es el encargado de propagar el contexto a sus Consumers hijos, a través de una prop llamada value. El hook useContext es el sustituto del componente Consumer, haciendo mucho más sencillo el acceso y la suscripción a los cambios del value del Provider.

Veamos un ejemplo. En primer lugar creamos el contexto con los datos del usuario, con un objeto vacío como valor inicial:

import { createContext } from 'react';
 
const UserContext = createContext({});
 
export default UserContext;

Luego utilizamos el Provider para que el componente hijo <Button/> tenga acceso al contexto. Aquí sí le pasamos un usuario (userMock) a través de la prop value.

import React from 'react';
import UserContext from './UserContext';
import Button from './Button';
 
const userMock = {
 name: 'Alejandro',
 email: 'alejandro@example.com',
};
 
function ShowContextExample() {
 return (
   <UserContext.Provider value={userMock}>
     <Button />
   </UserContext.Provider>
 );
}

Finalmente, en el componente Button, utilizando el hook useContext, accedemos a los datos que definimos como globales, en este caso, el nombre del usuario:

import React, { useContext } from 'react';
import UserContext from './UserContext';
 
function Button() {
 const { name } = useContext(UserContext);
 return (
   <button type="button">
     {name}
   </button>
 );
}

Nunca utilices los hooks de manera condicional

Tal y como funcionan los hooks, para evitar errores, es fundamental que en cada renderizado estos se ejecuten siempre en el mismo orden. De esta manera React puede mantener correctamente la relación entre el estado y su correspondiente llamada a useState y useEffect. Si alterásemos el orden de las llamadas a los hooks entre renderizados, React ya no sabría a qué llamada corresponde qué estado.

Para prevenir este comportamiento erróneo hay que evitar el uso de los hooks dentro de condicionales o funciones anidadas. En cambio, sí deben utilizarse siempre en el nivel superior de los componentes.

Siguiendo con el ejemplo anterior, nunca debería hacerse lo siguiente:

if (articles !== undefined) { // 🔴 Esto rompe la regla para usar hooks.
   useEffect(() => {
     fetch(apiRoutes.favourites)
     .then(res => res.json())
     .then(response => setArticles(response.articles));
   }, [])
 }

Tus propios hooks y más…

Existen más hooks incorporados en React y además es sencillo crear tus propios hooks para extraer lógica y reutilizar funciones en distintos componentes. Eso sí, es importante mantener la convención para nombrar tus custom hooks, utilizando la palabra use al comienzo (use<NombreDelHook>), para que React pueda ayudarte a comprobar que no estás infringiendo ninguna regla en el uso de los hooks.

Conclusión

Los hooks suponen una adición a lo que ya existía y por lo tanto no implican el fin de los componentes de clase. Es decir, no es necesario refactorizar todo un proyecto hecho con clases para pasarlo a componentes funcionales con hooks, sino que pueden coexistir ambos. Lo que sí nos recomienda la propia documentación de React es que, una vez familiarizados con esta nueva API —y si la versión de React del proyecto lo permite—, los nuevos componentes que desarrollemos sean funcionales y evitemos las clases, tal y como lo hacen actualmente en el equipo de desarrollo de Facebook.

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.