Antes del lanzamiento de los Hooks en React, había dos formas principales de crear componentes: usando clases o funciones.
Los componentes basados en clases son capaces de definir las propiedades del estado y los métodos para el ciclo de vida. En cambio los componentes funcionales (basados en funciones) no pueden acceder al estado o al ciclo de vida de React, aunque si a los props
al igual que las clases. Esto quiere decir que dependen de los padres para tener acceso a los datos de entrada.
Eso dio nombre a esos dos tipos de componentes:
- Componentes con estado (Stateful components).
- Componentes sin estado (Stateless components).
Stateful Components
Este tipo de componentes permite tener un ciclo de vida y estado. Normalmente son creados a partir de clases.
import React, { Component } from "react";
class Stateful extends Component {
constructor(props) {
super(props);
this.state = {
hello: "Hello World!"
};
}
updateText = () => {
this.setState({
hello: "Hello!"
});
};
render() {
return <button onClick={this.updateText}>{this.state.hello}</button>;
}
}
export default Stateful;
Stateless Components
No dependen de tener un estado, ni un ciclo de vida, solo van a presentar lógica. Normalmente son creados a partir de funciones.
import React from "react";
function Stateless(props) {
return(
<h1>Hello {props.text}!</h1> // Hello World!
);
};
export default Stateless;
¿Qué son los Hooks?
Los Hooks son una característica que se incorporó en React 16.8, permiten usar el estado y otras funciones de React sin tener que definir una clase de JavaScript. Es otra forma de poder crear Stateful Components aprovechando la sintaxis de las funciones.
Para explicarlo mejor, he aquí un simple contador para aumentar o disminuir un número.
Sin Hooks
import React, { Component } from "react";
class Counter extends Component {
constructor(props) {
super(props);
this.state = {
counter: 0
};
}
render() {
return (
<div>
<span>{this.state.counter}</span>
<button
onClick={() => this.setState({ counter: this.state.counter + 1 })}>
Click +1
</button>
<button
onClick={() => this.setState({ counter: this.state.counter - 1 })}>
Click -1
</button>
</div>
);
}
}
export default Counter;
Con Hooks
import React, { useState } from "react"; // importación del Hook useState
function Counter() {
const [ counter, setCount ] = useState(0);
// counter = valor del estado
// setCount = método para actualizar el estado
// parámetro = valor inicial del estado (0)
return (
<div>
<span>{ counter }</span>
<button onClick={ () => setCount(counter + 1)}>Click +1</button>
<button onClick={ () => setCount(counter - 1)}>Click -1</button>
</div>
);
}
export default Counter;
Si no entiendes la sintaxis de [counter, setCount]
, a eso se le llama desestructuración y es parte del lenguaje de JavaScript. Te dejo mi BitSnack para que entiendas mejor de que se trata.
La sintaxis es mucho más simple y limpia, lo que facilita la lectura del componente. La gran ventaja es para personas que se van introduciendo a React e incluso a JavaScript, va a ser más fácil entender lo que esta pasando.
La diferencia más grande radica en los Hooks. Pues se esta importando y haciendo uso del useState
para añadir un estado local al componente.
useState
Como has visto hace un momento el Hook useState
permite añadir un estado local al componente.
const [ estado, establecerEstado ] = useState(estadoInicial);
estado
: El valor del estado.establecerEstado
: Método para actualizar el estado, lo que se haga aquí va a definir el nuevo valor del estado (estado
).estadoInicial
: El valor inicial del estado (estado
).
Si en verdad te esta costando mucho entender la desestructuración, te dejo un artículo a detalle de la MDN.
Tomando el ejemplo anterior, lo que se esta haciendo es definir un contador (counter
) y un método para actualizarlo (setCount
) con un valor inicial de 0
.
const [ counter, setCount ] = useState(0);
Lo siguiente aquí es ver como se hace uso del setCount
de dos maneras:
- Para aumentar el contador.
- Para disminuir el contador.
<button onClick={ () => setCount(counter + 1)}>Click +1</button>
<button onClick={ () => setCount(counter - 1)}>Click -1</button>
Al final tienes un componente que vive perfectamente en armonía.
useEffect
Es un Hook que recibe una función para ejecutarse justo después de que se haya mostrado el renderizado en pantalla, aunque también tiene la opción de activarse cuando cambian ciertos valores.
import React, { useState, useEffect } from "react";
function Counter() {
const [ counter, setCount ] = useState(0);
// Actualiza el título del sitio, después del renderizado.
// Similar a componentDidMount y componentDidUpdate.
useEffect(() => {
document.title = `Click: ${counter}`;
});
return (
<div>
<span>{ counter }</span>
<button onClick={ () => setCount(counter + 1)}>Click +1</button>
<button onClick={ () => setCount(counter - 1)}>Click -1</button>
</div>
);
}
export default Counter;
Básicamente lo que hace es manejar componentDidUpdate
, componentDidMount
y el componentWillUnmount
, todo en uno.
¡Perfecto! Ahora te presento un ejemplo más probable trayendo datos desde una API.
import React, { useState, useEffect } from "react";
function Items() {
const [list, setList] = useState();
useEffect(() => {
async function getData(url) {
const response = await fetch(url);
const result = await response.json();
setList(result);
}
getData("site.com/api/");
});
return(
<ul>
{
list && list.map(item => <li>{item.name}</li>)
}
</ul>
);
}
export default Items;
¡Bien!, El Hook va a hacer un llamado a la API justo después de renderizar el componente para posteriormente dibujar los datos. Solo que...
Hay un pequeño problema y es que si se ejecuta el código, se va a estar haciendo la petición constantemente, convirtiendo la app en un loop infinito.
Esto sucede porque a pesar de que el Hook useEffect()
sabe que hacer, no tiene alguna referencia o dato que le diga en que momento hacerlo, es decir, no tiene una forma de saber cuando parar o continuar con el proceso. Para evitarlo, hay que hacer uso del segundo parámetro, que define cuándo se actualiza el componente.
useEffect(() => {
async function getData(url) {
const response = await fetch(url);
const result = await response.json();
setList(result);
}
getData("site.com/api/");
}, []); // Solo ejecuta useEffect la primera vez
¿Qué significa esto? Bueno, para explicarlo, voy a tomar el primer ejemplo de useEffect
, agregándole un segundo parámetro también.
useEffect(() => {
document.title = `Click: ${count}`;
}, [counter]); // Se le agrega el parámetro [counter]
Si counter
en un inicio tiene algún valor (5
) y posteriormente nuestro componente se vuelve a renderizar con el mismo valor (5
), React lo que va a ser es comparar el valor del renderizado anterior con el siguiente. Debido a que todos los elementos del array son iguales, React va a omitir el efecto. Saliendo de ese loop infinito.
En cambio, cuando se renderiza counter
con un valor diferente (6
), React va a volver a actualizar los datos. Porque en teoría se estarían actualizando, incluso si en el array cambio solo un valor.
Básicamente el comportamiento que se esta viendo es el de componentDidMount
, componentWillUnmount
y si se llegan a actualizar los datos componentDidUpdate
.
Opcionalmente el argumento puede tener un array vació, como se ve el ejemplo de la API. Haciendo que solo exista componentDidMount
y componentWillUnmount
, para que solo se renderice la primera vez. Esto le dice a React que el efecto no depende de ningún valor de las props
o el estado.
Otros Hooks
useContext
: Acepta un objeto de contexto, el valor devuelvo porReact.createContext
, posteriormente devuelve el valor de contexto actual para ese contexto en concreto.useReducer
: Para manejar estados complejos y transiciones de estado. Una alternativa auseState
.useCallback
: Devuelve una función callback que se memoriza y que solo cambia si cambia una dependencia del árbol de dependencias.useMemo
: Devuelve un valor memorizado.useRef
: Devuelve un objeto ref mutable. El objeto devuelto estará disponible durante toda la vida útil del componente.useImperativeHandle
: Para personalizar el valor de la instancia que está disponible para los componentes principales, cuando se usan referencias en React.useLayoutEffect
: Es idéntico auseEffect
, pero se detona sincrónicamente después de todas las mutaciones del DOM. También se renderiza de la misma forma quecomponentDidUpdate
ycomponentDidMount
.useDebugValue
: Se puede usar para mostrar una etiqueta en los Hooks personalizados en React DevTools.
Construye tu propio Hook
Ahora que ya sabes que es un Hook y ya conoces los 2 más básicos, ¿qué te parece si construyes el tuyo? Hacer tus propios Hooks permite crear la lógica de componentes en funciones reutilizables.
Por ejemplo, tener dos botones que hagan completamente lo mismo, tener sus propios contadores y hacer uso de dos useState
para guardar los valores por separado.
import React, { useState } from "react";
function Counter() {
const [counter, setCount] = useState(0);
const [counterTwo, setCountTwo] = useState(5);
return (
<div>
<span>{counter}</span>
<span>{counterTwo}</span>
<button value={counter} onClick={() => setCount(counter + 1)}>Click +1</button>
<button value={counterTwo} onClick={() => setCountTwo(counterTwo + 1)}>Click +1</button>
</div>
);
}
export default Counter;
Como se puede ver en el ejemplo se utiliza useState
dos veces para que cada contador tenga su propio estado, posteriormente se utilizan dos funciones de actualización para cada uno.
Simplifiquemos esto con la creación de un Hook personalizado para manejar este tipo de interacciones con una sola función.
La convención para nombrar un Hook es usar use
, en conjunto con la descripción de la función, por ejemplo useCounter
.
import { useState } from "react";
export function useCounter(initialValue) {
const [value, setValue] = useState(initialValue);
return {
value,
onClick: () => setValue(value + 1),
};
}
Se hace exactamente lo mismo, con la diferencia de que se devuelve el valor y la función onClick
para crear el evento y actualizar el valor.
Ahora el componente cambia y usa el nuevo Hook personalizado.
import React, { useState } from "react";
import { useCounter } from "./useCounter";
function Counter() {
const counter = useCounter(0);
const counterTwo = useCounter(5);
return (
<div>
<span>{counter.value}</span>
<span>{counterTwo.value}</span>
<button {...counter}>Click +1</button>
<button {...counterTwo}>Click +1</button>
</div>
);
}
export default Counter;
De esta forma cada vez que se requiera utilizar un contador, basta con llamar al Hook useCounter
.
Beneficios
- Una sintaxis más limpia: Como se vio en un principio, los Hooks proporcionan un código más limpio y fácil de entender. Las clases son complejas, incluso al entenderlas, su lectura y entendimiento es más difícil en comparación con una función y esto es más notorio a medida que el componente crece.
- Reutilizar código: Al crear un Hook personalizado te podrás haber dado cuenta que reutilizamos cierta parte del código en una sola función, eso es el gran poder que tienen los Hooks, el convertir algo repetitivo en algo funcional.
- Código optimizado (Babel): Si utilizas un compilador, digamos Babel para utilizar código JS moderno, sabrás que las clases posteriormente pasan a ser funciones con un montón de código extra para que funcione. Bueno, los Hooks no necesitan de clases, por lo que ese código extra ya no sería necesario.
Reglas a tener en cuenta
Solo llama a los Hooks en el nivel superior
Esto quiere decir que no llames a un Hook dentro de bucles, condiciones o funciones anidadas. Así garantizas que los Hooks siempre se llamen en el mismo orden después de cada renderizado. De esta manera React va a preservar correctamente el estado de los Hooks entre múltiples llamadas. Si uno de los Hooks esta oculto en un bucle o condicional, el orden puede cambiar, arruinando la lógica del componente.
Solo llama a los Hooks desde funciones de React u Hooks personalizados
Es simple, no llames a los Hooks desde funciones de JavaScript.