React Hooks

Los hooks es una de las características más aplaudida por la comunidad de React, pues ha venido a revolucionar por completo la forma en que construimos componentes, al mismo tiempo que ha corregido ciertos problemas que React arrastraba desde su lanzamiento, el cual tenía una sintaxis con poco confusa y un ciclo de vida algo complejo.

Si no dirigimos a la documentación oficial de React, podremos ver que los hooks con:

tip

Nuevo concepto: Hooks

Hooks son una nueva característica en React 16.8. Estos te permiten usar el estado y otras características de React sin escribir una clase.

Introducción a los Hooks

Como ya lo mencionamos anteriormente, se dijo que los hooks permite agregar estados a los componentes de función, sin embargo, ese es solo un comunicado de Marketing para que la gente pueda entender rápidamente que es, pero como también analizamos hace un momento, los componentes de clase tiene una serie de inconvenientes que son resueltos con los hooks, entonces, podríamos decir que los hooks son en realidad una nueva forma de escribir componentes que mejora la composición y reutilización del código.

Estado

Sin lugar a duda, usar estados en componentes de función es la característica que más nos han vendido, por lo que simplemente comenzaremos por allí.

Lo primero que tienes que saber sobre los Hooks es que absolutamente todo lo relacionado con ellos aplican sobre componentes de función, los que cuales anteriormente conocíamos como componentes sin estado (Stateless).

1import React from 'react'
2import Image from 'next/image';  
3    
4    function Example() {  
5        // Declara una nueva variable de estado, que llamaremos "count".  
6        const [count, setCount] = React.useState(0);  
7        
8        return (  
9        <div>  
10            <p>You clicked {count} times</p>  
11            <button onClick={() => setCount(count + 1)}>  
12            Click me  
13            </button>  
14        </div>  
15        );  
16    }  

El ejemplo que vemos arriba es el ejemplo más ampliamente usado para demostrar el uso de los estados con los Hooks, y lo hemos obtenido directamente de la documentación oficial de React.

Lo primero que veremos será el Hook useState, el cual nos permite definir un estado para el componente. useState recibe como parámetro el valor inicial del estado y retorna un array de dos posiciones, donde la primera posición corresponde al valor actual del estado, mientras que la segunda posición corresponde a una función para actualizar estado.

La línea 5 puede ser especialmente confusa, por que utiliza algo llamada desestructuración, el cual consiste en declarar y asignar los valores del array según su posición. Para entender mejor que está pasando veamos un ejemplo totalmente equivalente al anterior:

1import React, { useState } from 'react';  
2    
3    function Example() {  
4        
5        const state = useState(0)  
6        const count = state[0]  
7        const setCount = state[1]  
8        
9        return (  
10        <div>  
11            <p>You clicked {count} times</p>  
12            <button onClick={() => setCount(count + 1)}>  
13            Click me  
14            </button>  
15        </div>  
16        );  
17    }  

Observa como useState regresa un array y luego asignamos el valor de cada posición en una variable diferente. Eso que vemos es exactamente lo que evitamos hacer con la desestructuración, para evitar un código muy verboso.

Una vez comprendido lo anterior, es importante mencionar que la posición 1 corresponde al valor del estado y la segundo es una función para actualizar dicho estado, sin embargo, el nombre de las constantes creadas es totalmente arbitrario, ya que puedes tener algo parecido a lo siguiente y dará el mismo resultado:

1const [state, setState] = useState(0)  
2    
3    Ó
4        
5    const [manzana, setManzana] = useState(0)  
6        
7    Ó
8        
9    const [estado, establecerEstado] = useState(0)  

Observa que el nombre que le pongamos a cada valor es totalmente irrelevante, lo único realmente importante es que regresa un array de dos posiciones, donde el primer valor es el valor del estado actual y el segundo es una función para actualizar el estado.

Dicho lo anterior, regresemos al ejemplo original:

1import React from 'react'
2import Image from 'next/image';  
3    
4    function Example() {  
5        // Declara una nueva variable de estado, que llamaremos "count".  
6        const [count, setCount] = React.useState(0);  
7        
8        return (  
9        <div>  
10            <p>You clicked {count} times</p>  
11            <button onClick={() => setCount(count + 1)}>  
12            Click me  
13            </button>  
14        </div>  
15        );  
16    }  

Observa que el componente renderiza un botón que cuando le damos click hace una llamada a la función setCount, a la cual le incrementamos el valor del contador de clicks.

Lo primero que podemos observar es la ausencia de un constructor para inicializar el valor del estado, pues de define desde el momento de crear el estado con useState, en segundo lugar, ya no hace falta utilizar la instrucción this para hacer referencia a la función para actualizar el estado.

Un detalle importante al momento de actualizar el estado mediante Hooks, es que, el estado anterior no se mescla con el nuevo estado, por lo tanto, siempre que actualicemos el estado deberemos enviarle el valor final con el que quedará. Por ejemplo, vemos que pasa cuando actualizamos el estado con un componente de clase:

1// constructor
2this.state = {  
3    a: 1,  
4    b: 2  
5}  
6
7// componentDidMount
8this.setState({  
9    a: 5  
10})  
11console.log(this.state) // {a: 5, b: 2}  

Observa que el estado se mescla para crear un nuevo estado de la unión del anterior con el nuevo, sin embargo, en los Hooks no pasa esto. Veamos un ejemplo:

1const [state, setState] = React.useState({  
2    a: 1,  
3    b: 2  
4})  
5    
6// Any section  
7setState({   
8    a: 5  
9})  
10console.log(state) // {a: 5}  

Para realizar una correcta actualización del estado respetando los valores anteriores podemos utilizar el operador de propagación (Spread operator), el cual consiste en anteponer 3 puntos (…) al estado para extraer sus propiedades y agregarlas al nuevo estado, veamos un ejemplo:

1const [state, setState] = React.useState({  
2    a: 1,  
3    b: 2  
4})  
5    
6    
7setState({   
8    ...state,  
9    a: 5  
10})  
11console.log(state) // {a: 5, b: 2} 

Este pequeño truco hace que todas las propiedades del estado actual pasan al nuevo estado, y luego actualizo puntualmente la propiedad que queremos actualizar.

Un último detalle interesante de los hooks es que permite agregar múltiples estados:

1import React from 'react'
2import Image from 'next/image';  
3
4function Example() {  
5// Declara una nueva variable de estado, que llamaremos "count".  
6const [count, setCount] = React.useState(0)   //😍
7const [load, setLoad] = React.useState(false) //😍
8
9    return (  
10        <div>  
11        <p>You clicked {count} times</p>  
12        <button onClick={() => setCount(count + 1)}>  
13            Click me  
14        </button>  
15        </div>  
16    );  
17} 

Observa que hemos creado un segundo estado que guarda una bandera para saber si la página ha terminado de cargar, la cual tiene su propio método set.

Ciclo de vida

Otra de las limitantes de los compontes de función era que no tenía un ciclo de vida, por lo tanto, solo funcionaban como componente de representación, es decir, tomaban los props y luego renderizaban la vista a partir de ellos.

Con los Hooks eso cambia, pues podemos agregar algo llamado efectos especiales o simplemente efectos (effects), los cuales son un tipo especial de hooks que permite agregar comportamiento a los componentes de función. De forma resumida, un efecto combina los métodos componentDidMount, componentDidUpdate y componentWillUnmount en una sola API, lo que simplifica significativamente el ciclo de vida. Veamos un ejemplo:

1import React, { useState, useEffect } from 'react';  
2    
3    const MyComponent = (props) => {  
4            
5        const [user, setUser] = useState()  
6            
7        // Similar a componentDidMount y componentDidUpdate:  
8        useEffect( () => {  
9            fetch('/users/' + props.user)
10                .then(res => res.json())
11                .then(res => setUser(res))
12        })
13
14        return(
15            <p>{user.name}</p>
16        )
17    }
18    export default MyComponent  

En la línea 8 podemos ver como se declara un effect, y podemos ver también que, recibe como parámetro un arrow function, que es justo donde pondremos nuestra lógica. Cabe mencionar que los effects se ejecutan justo después del renderizado del componente, ya sea cuando se monta por primera vez o se actualiza, es por ello que cuando se ejecuta un effect, todos los elementos del DOM ya están presentes en el navegador.

Si analizamos el ejemplo anterior, podemos ver que este efecto actúa como el método componendDidMount y componentDidUpdate al mismo tiempo, pues se ejecutará justo después del primer renderizado, pero también se ejecutará durante la actualización.

Un detalle muy importante a tomar en cuenta es que, los effects se ejecutarán después del renderizado del componente, pero también se ejecutarán cuando el estado cambie, por esta razón, si ejecutamos el código anterior, veremos que el effect se ciclará, pues se ejecutará la primera vez después del montado inicial, luego llamará al API para consultar un usuario, luego, actualizará el estado. Con la actualización del estado el componente nuevamente se renderizará, con lo que el effect será nuevamente ejecutado y así de forma indeterminada.

Para evitar este comportamiento, los efectos reciben un segundo argumento, el cual es un arreglo con valores que serán evaluado para determinar si el efecto requiere ser ejecutado de nuevo. Por default un efecto siempre se ejecutará la primera vez, pero seguido de esto, los valores que sean enviados en este arreglo serán comparados con el valor cuando se ejecuten la segunda vez, si algún valor es diferente al de la ejecución anterior, entonces el efecto se ejecutará de nuevo, y si todos son igual, el efecto ya no se ejecuta. Veamos el ejemplo anterior con este pequeño ajuste:

1import React, { useState, useEffect } from 'react';  
2    
3    const MyComponent = (props) => {  
4            
5        const [user, setUser] = useState()  
6            
7        // Similar a componentDidMount y componentDidUpdate:  
8        useEffect( () => {  
9            fetch('/users/' + props.user)
10                .then(res => res.json())
11                .then(res => setUser(res))
12        }, [props.user] )
13
14        return(
15            <p>{user.name}</p>
16        )
17    }
18    export default MyComponent  

En la línea 12 hemos agregado un arreglo con el nombre del usuario, lo que hará que el efecto se ejecute la primera vez, pero después de esto, solo se volverá a ejecutar cuando el valor de la propiedad cambie.

Toma en cuente que este array puede tener varias posiciones, permitiendo tener múltiples razones por las que el efecto debería de ejecutarse.

tip

Error común

Un error común es agregar expresiones para validar, por ejemplo props.user!=null, ya que el efecto se ejecutará cuando el valor cambie con respecto a la ejecución anterior, por lo tanto, si la expresión retorna true y pero el resultado de la expresión pasada era false, entonces el valor ha cambiado y el efecto se ejecuta, y en el caso contrario pasaría lo mismo.

De la misma forma que podemos tener varios estados, también podemos tener diferentes efectos, y cada efecto puede tener reglas diferentes para que se ejecuten bajo diferentes circunstancias, solo toma en cuenta que los efectos serán ejecutados en el orden en que están declarados:

1import React, { useState, useEffect } from 'react';  
2    
3    const MyComponent = (props) => {  
4            
5        const [user, setUser] = useState()  
6        const [tweets, setTweets] = useState([])  
7            
8        // Similar a componentDidMount y componentDidUpdate:  
9        useEffect( () => {  
10            fetch('/users/' + props.user)
11                .then(res => res.json())
12                .then(res => setUser(res))
13        }, [props.user])
14
15        // Similar a componentDidMount y componentDidUpdate:
16        useEffect( () => {
17                        fetch('/tweets')
18                            .then(res => res.json())
19                            .then(res => setTweets(res))
20                    }, [ ])
21
22        return(
23            <p>{user.name}</p>
24        )
25    }
26    export default MyComponent  

Observe que en esta ocasión tenemos dos efectos, con la única diferencia de que las reglas para ejecutarlos cambian y aquí es donde quiero darles un tip, si quieres que un efecto solo se ejecuta una vez, agrega un arreglo vacío, así los valores nunca cambiarán y dará como resultado la ejecución inicial justo después del primer render.

tip

Tip

Un arreglo vacío garantiza que un efecto solo se ejecuta una vez, justo después del primer renderizado.

Si prestante atención, dijimos que los efectos funcionan como el método componentDidMount, componentDidUpdate y componentWillUnmount, sin embargo, creo que hasta ahora hemos comprobado que funciona como los primeros, pero que pasa con el componentWillUnmount, ¿será que también se va a ejecutar el efecto antes de desmontarse?, la respuesta es sí y no, me explico.

Un efecto puede retornar una Function, la cual conocemos como funcione de limpieza, y esta funcion se ejecutan cuando el componente es desmontado, sin embargo, recordemos que un efecto se puede ejecutar varias veces, lo que nos lleva a que React en realidad ejecute la función de limpieza antes del siguiente renderizado, de esta forma, es como si el componente se desmontara justo antes del renderizado para luego ejecutar el efecto. Veamos mejor un ejemplo de esto:

1import React, { useState, useEffect } from 'react';  
2    
3    const MyComponent = (props) => {  
4            
5        const [user, setUser] = useState()  
6        const [tweets, setTweets] = useState([])  
7            
8        // Similar a componentDidMount y componentDidUpdate:  
9        useEffect( () => {  
10            fetch('/users/' + props.user)
11                .then(res => res.json())
12                .then(res => setUser(res))
13
14            return () => {
15                        console.log("clean action")
16                        //any actions  
17                    }
18        }, [props.user])
19
20        return(
21            <p>{user.name}</p>
22        )
23    }
24    export default MyComponent  

Utilizar funciones de limpieza es poco común, así como utilizar el método componentWillUnmount, sin embargo tiene algunos usos, como eliminar algunos listener, limpiar algún elemento de la pantalla, etc.

Creando nuestros propios Hooks

Al inicio de esta unidad hablamos de que una de las principales problemáticas de React era que no tenía un mecanismo simple para reutilizar la lógica no visual, lo que nos llevaba al infierno de los envoltorios, donde teníamos que crear componentes que retornaban otro componente, pues bien, uno de los principales objetivos de los Hooks es precisamente proporcionar un mecanismo que nos permita crear nuestros propios Hooks, los cuales pueden tener cierta lógica no visual y reutilizable.

Antes de continuar, es importante comprender algo, un Hook es una función común y corriente la cual no tiene una firma específica, es decir, se puede llamar como sea, recibir los argumentos que sea y regresar lo que sea, sin embargo, hay una regla no escrita que dice que todos los hooks deben de comenzar con la palabra “use”, como es el caso de useState, useEffect, useContext, etc. Podrás ver entonces que todas estas funciones también son Hooks.

Cuando digo que es una regla no escrita me refiero a que React o JavaScript no podrán impedir que les pongas otro nombre, sin embargo, si está definido en la documentación de React, por lo tanto, se podría decir que debemos de comenzar los hooks con “use” aunque nadie nos puede impedir que lo hagamos.

En esencia un hooks es una función que encapsula una lógica no visual encapsulada en una función que podemos reutilizar sin necesidad de caer en el infierno de los envoltorios o los conocidos como High Order Components. Pero vasta de palabrerías y vamos a un ejemplo de cómo crear nuestro propio hook:

1import { useState, useEffect } from 'react';  
2import APIInvoker from '../utils/APIInvoker'  
3    
4function useTweets(username) {  
5    const [tweets, setTweets] = useState([]);  
6    
7    useEffect(() => {  
8        let url = username ? '/tweets/' + username : '/tweets'
9        APIInvoker.invokeGET(url, response => {setTweets(response.body)})
10    }, [username]);
11
12    return tweets;
13}
14export default useTweets  

Este hook nos permite recuperar los últimos tweets o los tweets de un solo usuario. Para lograr esto, requiere que se le envié el usuario del cual se buscarán los tweets, pero si ese valor no se envía, se buscan los últimos tweets de todos los usuarios. Ahora bien, para utilizar el hook anterior, solo tenemos que mandarlo llamar:

1import React, { useState, useEffect } from 'react';  
2import useTweets from './hooks/useTweets'  
3const MyComponent = (props) => {  
4        
5    const tweets = useTweets("oscar")  
6        
7    return(  
8        <div>  
9            {tweets.map(x => <p key={x._id}>{x.message}</p>)}  
10        </div>  
11    )  
12}  
13export default MyComponent  

Justo antes de que el componente sea renderizarse, el hook useTweets será ejecutado, y con ello, el valor por defecto será asignado, es decir un array en blanco, luego el efecto del hook se ejecutará, actualizará el valor del estado (setTweets) y luego el nuevo valor será asignado a la variable tweets.

Para reutilizar el hook useTweets solo hace falta llamarlo desde otro componente, y con esto, estaríamos reutilizando lógica no visual de forma simple.

Utilizando el Context con los Hooks

Uno de los hooks más populares es useContext, el cual nos permite utilizar el Context sin la necesidad de encapsular nuestro componente dentro de un Consumer, evitando el famoso infierno de las envolturas, veamos un ejemplo:

1import React, { useContext } from 'react'  
2import UserContext from './context/UserContext'  
3    
4const MyComponent = (props) => {  
5        
6    const userContext = useContext(UserContext)  
7        
8    return(  
9        <p>{userContext.username}</p>  
10    )  
11}  
12export default MyComponent  

Si observas la línea 6, podrás ver que es posible recuperar el contexto con tan solo utilizar el hook useContext, el cual recibe como parámetro el contexto que queremos recuperar.

El equivalente a este componente sin hooks sería algo así:

1import React, { useContext } from 'react'  
2import UserContext from './context/UserContext'  
3    
4const MyComponent = (props) => {  
5            
6    return(  
7        <UserContext.Consumer>  
8            {userContext =>   
9                <p>{userContext.username}</p>  
10            }  
11        <UserContext.Consumer/>  
12    )  
13}  
14export default MyComponent  

Ahora bien, si necesitáramos utilizar más de un Context, entonces tendríamos que envolver nuestro componente en otro Consumer, lo que va haciendo cada vez más complicado el componente. En cambo con hooks, simplemente utilizamos useContext con todos los contextos que necesitemos.

Algo a tomar en cuenta, es que a pesar de utilizar los hooks para obtener el contexto, es que solo nos ahorra la creación del Consumer, pero seguiremos necesitando crear el Provider, de por lo que cuando usemos useContext, deberá ser sobre un componente que sea descendiente del Provider.

El futuro de las clases

Muchos se ha especulado que ahora con la llegada de los hooks, los componentes de clase pasaran a ser posteriormente desaconsejados (deprecados), sin embargo, el mismo equipo de React ha salido a desmentir esto, incluso, insisten en que no hay planes para desaconsejar el uso de clases en el futuro, por lo que podemos seguir usando clases en nuestros proyectos la garantía de que seguirán por mucho tiempo más.

Otro de los comentarios que puedes ver mucho en Internet es personas diciendo que hora debemos de mirar todos nuestros componentes para utilizar Hooks, sin embargo, pueden existir proyectos que combinen componentes de clases y componentes de función, incluso, es normal ver proyectos donde existen componentes de función y de clase al mismo tiempo.

Mi recomendación es que solo migremos a hooks solo los nuevos compontes que creemos y migrar uno que otro componente de clase que se nos complique por el uso de clases, pero fuera de eso, no tiene caso hacer una migración total del proyecto.

Acerca de este libro

Aplicaciones reactivas con React, NodeJS & MongoDB

Todo lo que acabas de ver en este artículo es solo una pequeña parte del libro Aplicaciones reactivas con React, NodeJS & MongoDB, El libro más completo en español para aprender a crear aplicaciones web completas con las tecnologías más potentes de la actualidad, desde el Frontend con React, hasta el Backend con un poderoso API REST con NodeJS y Express y persistiendo todo en MongoDB. te invito a que veas mi libro:

Ver libro
Todos los derechos reservados ©
Reactive programming
LinkedinYoutubeTwitterFacebook

© 2021, Copyright - Oscar Blancarte. All rights reserved.