React — Hooks [useReducer, useCallback, useMemo, useRef, and customs Hooks](Parte II)
Temario
- Introducción
- Hooks adicionales (useReducer, useCallback, useMemo, useRef)
- Customs Hook
i. Introducción
Anteriormente explicamos que es un Hook, así como los Hooks incorporados (useState, useEffect, useContext)[ref].
Ahora veremos los Hooks adicionales, que básicamente son variantes de los Hooks incorporados y que son necesarios para casos muy específicos.
:: Tipos de Hooks
Recordemos que tenemos tres grupos de Hooks:
— Incorporados: useState, useEffect, useContext
— Adicionales: useReducer, useCallback, useMemo, useRef, useImperativeHandle, useLayoutEffect, useDebugValue
— Otros: Hooks personalizados
:: Reglas básicas
No debemos olvidar las reglas básicas de los Hooks:
- Los Hooks no funcionan con componentes de clase.
- Nunca llamar un Hook dentro de un loop, condición, función anidada o funciones fuera del componente, con esta regla nos aseguramos de que los Hooks siempre se llamen en el mismo orden cada vez que un componente se renderiza.
- Los Hooks, deben estar ubicados dentro del componente en el nivel superior.
- Solo usar cuando sea un componente funcional.
- Un Hook puede mandar a llamar a otro Hook.
ii. Hooks adicionales
— useReducer
Es preferible usar
useReducer
en vez deuseState
cuando se tiene una lógica de estado compleja que involucra múltiples subvalores, o cuando el siguiente estado depende del anterior.[ref]
Cuando comencé a investigar useReducer
, vi muchas “discusiones” de cuándo usar useReducer
ó useState
. Llegue a la conclusión que useState
es el mejor para un estado simple con lógica de actualización simple y useReducer
es el mejor para un estado complejo con la lógica de actualización compleja. Pero… ¿cuándo es un estado “complejo” o “simple”?, ¿Qué entendemos por lógica “compleja”?… si te has hecho alguna o todas estas preguntas, continúa leyendo…
:: ¿Cómo se importa?
Importamos la función en la parte superior del componente de la siguiente manera:
Al ser un paquete de React (y no ser el de default) se va a agregar usando las llaves {}
, por lo que le estaremos diciendo a nuestro componente funcional que va a manejar un estado local.
:: ¿Cuál es su sintaxis?
Y para la función reducer
:
:: Ejemplo 1
Vamos a crear un contador, donde vamos a tener dos botones (uno que aumente, y otro que disminuya el número):
Para entenderlo, vamos a hacerlo primero con useState
…
Lo primero es crear un proyecto con create-react-app
llamado app-counter
Si tienes dudas por que npx o llegaste aquí directo, te recomiendo que inicies primero por acá (Mi primera App con create-react-app [ref])
Creamos el componente Components/Counter
(si estás utilizando visual code e instalaste los plugins, solo basta escribir rafce
dentro del archivo Counter.jsx
y presionar enter).
Ahora vamos a crear el useState
counter
, y dos funciones (uno para incrementar el estado, y el otro para disminuirlo):
Podríamos quizás reducir el código:
Por último importamos y agregamos Counter
en la App
:
Vamos añadirle complejidad a este ejemplo….agregamos dos botones más: (uno va a resetear el contador, y el otro va a congelar el contador [cuando incrementemos, disminuyamos o reiniciemos, no va a hacer nada]):
Entonces, agregamos los dos botones, y hacemos sus acciones:
Simplifiquemos el código con el operador &&
:
Quizás, para no tener tantas funciones, podríamos crear uno, y dependiendo del tipo, realizar una acción, algo así:
Precisamente la lógica de arriba, es lo que hace useReducer
, veamos el mismo ejemplo. Tenemos los 4 botones:
Ahora, importamos useReducer
:
El siguiente paso, es crear el estado local con useReducer
:
Donde:
counter
— Es el valor del estado actual.dispatchCounter
— Función que utilizaremos para setear el estado, donde le tenemos que mandar el tipo de acción que debe ejecutar.reducerCounter
— Función que lanzadispatchCounter
, para cambiar el estado decounter
.{count:0, frozen:false}
— Estado inicial decounter
.
Vamos a crear el función reducerCounter
:
La idea es que dependiendo del tipo action.type
, es la acción que va a realizar.
Se que estas pensando en reducir INCREMENT
y DECREMENT
, en algo así:
No es recomendable, ya que la idea de usar useReducer
, es siempre tratar de ser lo más explícitos con las transiciones de estado. Tener las transiciones de estado separadas (por tipo), nos va a permitir entender mejor la lógica de negocio.
El siguiente paso es cuando se le de click a los botones, ejecutamos la función dispatchCounter
, con su respectivo type
:
El código completo, quedaría de la siguiente manera:
Inclusive, podemos mandarle más parámetros, imaginemos que desde las propiedades nos mandan el valor donde debe iniciar, y cada vez que lo inicializamos comience desde ahí:
Y en la App
:
:: Ejemplo completo
:: Ejemplo 2
Crearemos un formulario, donde:
- Actualizaremos el estado de los campos
user
ypassword
. - Validamos que ambos campos estén llenos, para desbloquear/bloquear el botón.
- Cuando se le dé al botón, vamos a mostrar un
Loading
. En caso de que sea exitoso/error mostramos un mensaje. - Haremos los estilos con
react-jss
. - Para manejar el estado vamos a usar
useReducer
.
La idea es tener algo así:
Creamos un proyecto con create-react-app
llamado form-with-usereducer
.
Instalamos el styled component de JSS(react-jss)
[ref]:
— Definiendo los componentes
El siguiente paso es definir qué componentes son los que vamos a construir, entonces:
App
— Va a ser nuestro componente principal, y va a ser el encargado de mostrarForm
Form
— Es el componente donde vamos a escribir un usuario y contraseña, además se encargará de mostrar/ocultar: un mensaje, el componenteLoading
, así como de simular el envío de datos.Loading
— Es una animación en CSS de loading.io[ref]
— Creando componentes
Vamos a crear el componente funcional src/Components/Form.jsx
Lo importamos y agregamos al JSX en el componente App.js
La idea, es construir el siguiente formulario en Form
:
Entonces, quedaría así:
Nota: Aún no hemos agregado nada de estilos, ni lógica, sólo el HTML.
Lo que sigue, es crear nuestro estado con useReducer
(No olviden importarlo):
Donde:
form
— Es donde van a estar los datos actualizados.dispatch
— Función que nos va a servir para ejecutarreducer
.reducer
— Función que va a tener el control del estado.initialArguments
— Los valores iniciales del estado.
Generamos nuestra constante initialArguments
:
Donde:
data
— Son los datos del formulariouser
ypassword
.loading
— Bandera que nos va a permitir mostrar/ocultar el botón y una animación.isValid
— Nos va a servir para bloquear/desbloquear el botón.status
— Identificar el estado actual de nuestro formulario.message
— Un mensaje a mostrar.
Agregamos la función reducer
, con los siguientes tipos:
ENTER_DATA
— Guardará los valoresuser
ypassword
, así como también vamos a validar ambos valores, para saber si bloqueamos/desbloqueamos el botón.UPDATE_STATUS
— Cuando se le da click al botón, vamos quitar el botón y mostrar un cargando y dependiendo la respuesta va a mandarSUCCESS
oFAILURE
SUCCESS
— En caso de que este bien, va quitar el cargando, y va a mostrar el mensaje “You’re going to get redirected in a second”ERROR
— En caso de que “no estén correcto los datos”, va a quitar el cargando, y va amostrar “Incorrect credentials. Please try again!”
Hagamos ENTER_DATA
— En los dos inputs
, en el atributo onChange
agregamos la función handleChange
, donde ejecutará dispatch
, mandando el nombre y valor del campo:
Entonces en la función reducer
, en el type:ENTER_DATA
, hacemos lo siguiente:
Como no quiero sobrecargar con tanto código, he decidido separar la validación de los campos en una función llamada noEmptyFields
, entonces, vamos a generar un helper en Helpers/helper.js
:
Lo importamos en el componente Form
:
Por último, agregamos la validación en el botón:
Hasta aquí, lo único que hace nuestra aplicación es guardar el formulario en el estado, así como la validación de los mismos:
Hagamos UPDATE_STATUS
— Cuando se le de click al botón por medio del atributo onClick
, vamos a ejecutar la función handleSubmit
, donde mostraremos el “cargando…” y además simularemos una petición con unsetTimeout
:
Entonces en la función reducer
, en el type:UPDATE_STATUS
, hacemos lo siguiente:
Y por último agregamos en el JSX
él “cargando…”:
Si hemos hecho bien todos los paso hasta aquí, nuestro ejemplo debe verse así:
Podemos observar, hasta que esté capturado el usuario y contraseña es cuando se habilita el botón, y al momento de darle click este se esconde y se muestra “Cargando…”.
Vamos a validar la parte del mensaje, nuestro código está actualmente así:
Hacemos la validación: si existe mensaje, lo muestre, caso contrario, no.
Hagamos SUCCESS
— Dentro del setTimeout
, vamos simular que los datos son correctos:
Entonces en la función reducer
, en el type:SUCCESS
, hacemos lo siguiente:
Hagamos FAILURE
— Dentro del setTimeout
, vamos simular que los datos son incorrectos:
Entonces en la función reducer
, en el type:FAILURE
, hacemos lo siguiente:
Si hemos hecho bien todos los paso hasta aquí, nuestro ejemplo debe verse así:
Ahora, lo siguiente, es agregar los estilos con react-jss
, entonces, para generar los estilos del formulario vamos a importarlo:
Nuestros estilos:
Dentro de nuestro componente ejecutamos useStyle
:
Y cambiamos : className="anyClass" => className={classes.anyClass}
Creamos el componente src/Components/Loading.jsx
Vamos a importar y agregar Loading
, en Form
:
El código completo de Form
:
:: Ejemplo completo
:: Descargar proyecto
Puedes clonar el ejemplo desde GitHub[ref].
:: Conclusión
Podemos decir que
useReducer
es otra alternativa para manejar los estados, donde, todos los cambios de estado se van a incluir en la función central llamadareductor
y el estado se actualizará de acuerdo con la acción indicada, así como el valor.
Es muy común iniciar con useState
, pero conforme se va aumentando la complejidad, terminemos refactorizando a useReducer
.
Quizás la regla, sería:
- Usar
useState
siempre que gestionemos un valor tipo primitivo de JavaScript (number, string, boolean…). - Usar
useReducer
siempre que administremos un objeto o arreglo.
Aunque las reglas de arriba NO siempre van a aplicar, ya que muchas veces va a depender de la lógica del negocio, como esté desarrollado el componente y las necesidades de la aplicación.
:: Ventajas y desventajas
- Cuando usamos
useReducer
, es más sencillo administrar un estado complejo y más viable entender el código. - Es más eficiente manejar el estado con
useReducer
. - Hacer pruebas unitarias a la función
reductor
, va a ser mucho más sencillo. - La lógica de estado, queda en una sola función , por lo que es más rápido darle mantenimiento.
- Usar
useReducer
, cuando surge la necesidad de tener una arquitectura de estado más predecible y mantenible
— useMemo
Es inevitable que en proyectos grandes, la aplicación llegué a tener problemas de rendimiento (independientemente de la excelente optimización que maneja internamente React), si en tu proyecto es muy importante el rendimiento o si eres de las personas que le gusta optimizar… te va a ser muy útil useMemo
.
useMemo
es un Hook que memoriza la salida de una función.
Su finalidad es validar si alguna de sus dependencias ha cambiado, en caso de que si haya cambiado, va a retornar el nuevo valor, caso contrario devolverá el valor que tiene en caché.
Veamos un ejemplo sin useMemo
:
Esta pequeña aplicación tiene 3 componentes App
, NameDisplay
y ExponentDisplay
; la idea es de que cuando capturemos un nombre, se vea reflejado, por lo otro lado, si capturamos un número, nos de el resultado de (number)^10
:: Ejemplo completo
Podemos observar que sin importar si estamos afectando el estado name
ó number
los componentes NameDisplay
y ExponentDisplay
se vuelven a renderizar; la idea es solo afecte aquel componente que cambien sus dependencias, así que usaremos React.seMemo
y React.memo
Importamos la función en la parte superior del componente de la siguiente manera:
Al ser un paquete de React (y no ser el de default) se va a agregar usando las llaves {}
, por lo que le estaremos diciendo a nuestro componente funcional que va a manejar un estado local.
:: ¿Cuál es su sintaxis?
Puede ser muy similar a useEffect
en el aspecto que ambos utilizan un arreglo de dependencias, pero NO DEBEMOS CONFUNDIRLOS, ya que useEffect
está destinado a los efectos secundarios (de ahí su nombre), mientras que useMemo
no tienen efectos secundarios (son funciones puras).
Si no le pasamos un arreglo de dependencias, se activará con cualquier dependencia (lo cual queremos evitar).
Es importante saber que cuando se usa o no
useMemo
, no debe cambiar el comportamiento de nuestra aplicación, excepto el rendimiento. Por lo que se recomienda primero escribir el código sin el Hook, y posteriormente agregarlo (si es necesario).
:: Ejemplo
En nuestro componente ExponentDisplay
, vamos a utilizar useMemo
, de la siguiente manera:
Podemos observar que cuando hacemos cambios en el estado de name
, los componentes se siguen renderizando, pero el cálculo está almacenado en caché, y sólo lo procesa cuando es necesario, en este caso es cuando escribimos 50 o 500
.
Ahora hagamos el cambio en el componente NameDisplay
. Actualmente está así:
Si utilizamos React.memo
:
Con React.memo
, es una forma de recordar todo el componente, por lo que solo renderiza cuándo las propiedades cambian.
:: Ejemplo completo
:: Ventajas y desventajas
- En la documentación de React[ref], no nos garantiza que realmente se llame cuando cambia alguna dependencia.
- El Hook agrega complejidad al código, por lo que puede salir contraproducente en el rendimiento.
- Es recomendable aplicarlo cuando los cálculos realmente salgan costosos, en caso de no estar seguro, se puede realizar de ambas maneras (sin y con el Hook), hacer pruebas y tomar una decisión.
- Solo utilizarlo en funciones puras.
- Todos los puntos anteriores aplican también para
React.memo
.
— useCallback
useCallback
va a memorizar una función (callback), mientras que useMemo
va a memorizar la salida de una función. El objetivo es no reinicializar la función, a menos que las dependencias hayan cambiando.
useCallback
es un Hook que memoriza la una función (callback).
Importamos la función en la parte superior del componente de la siguiente manera:
Al ser un paquete de React (y no ser el de default) se va a agregar usando las llaves {}
, por lo que le estaremos diciendo a nuestro componente funcional que va a manejar un estado local.
:: ¿Cuál es su sintaxis?
:: Ejemplo
Veamos el siguiente ejemplo sin useCallback
:
Donde:
- Tenemos dos contadores, que están almacenados en dos estados distintos
counter1
ycounter2.
- Donde nos permite incrementarlos mediante 2 botones, que al momento de darle click, cada uno va a ejecutar una función distinta (
increment1 ó increment2
), donde se va a incrementar un contadorsetCounter1 ó setCounter2
- Utilizamos
new Set()
[ref], para guardar las funciones que son únicas, con el propósito de saber cuantas funciones se crean mientras el usuario interactúa con la aplicación.
El objeto
new Set
te permite almacenar conjunto de valores únicos de cualquier tipo, incluso valores primitivos u objetos de referencia.[ref]
Podemos observar que la primera vez se han creado dos instancias de funciones (lo cual es correcto); pero al momento de darle click a uno de los botones, SIEMPRE crea dos nuevas instancias de funciones. Quizás, pueda parecer normal, ya que cada que cada vez que se hace el renderizado, vuelve a crear las funciones, pero, esta mal, ya que solo estamos afectado a uno de los estados…entonces la idea, es que solo se debe crear una nueva instancia y esto es cuando se actualice un valor dependiente.
Entonces, las funciones:
Vamos a encapsularlos con useCallback
:
- Le estamos indicando a
increment1
que mientrascounter1
no cambie, no es necesario que crea una nueva instancia. - Le estamos indicando a
increment2
que mientrascounter2
no cambie, no es necesario que crea una nueva instancia.
Podemos observar que la primera vez se han creado dos instancias de funciones (lo cual es correcto); y al momento de darle click a uno de los botones, crea solo UNA instancia, y es la que ha cambiado su dependencia.
:: Ejemplo completo
:: Ventajas y desventajas
- El Hook agrega complejidad al código, por lo que puede salir contraproducente en el rendimiento.
- Es recomendable aplicarlo cuando una función tenga dependencia con el estado.
- Se recomienda agregar la regla
exhaustive-deps
[ref] como parte deleslint-plugin-react-hooks
[ref] (advierte cuando las dependencias se especifican incorrectamente y sugiere una solución)
— useRef
useRef
devuelve un objeto ref mutable cuya propiedad.current
se inicializa con el argumento pasado (initialValue
). El objeto devuelto se mantendrá persistente durante la vida completa del componente. [ref]
:: ¿Cuál es su sintaxis?
Importamos la función en la parte superior del componente de la siguiente manera:
Al ser un paquete de React (y no ser el de default) se va a agregar usando las llaves {}
, por lo que le estaremos diciendo a nuestro componente funcional que va a manejar un estado local.
El Hook useRef
se usa en dos casos:
- Acceso a los
nodos DOM
o elementos React - Mantener una variable mutable.
:: Acceso a los nodos DOM o elementos React
Cuando ocupamos JavaScript y queremos acceder a un nodo DOM
, es muy común utilizar querySelector
, getElementById
,…[ref], pero si estamos desarrollando con React es recomendable utilizar useRef
.
Veamos el siguiente ejemplo, de como usarlo:
Donde:
useRef
es una función de enlace que se asigna a la variabletextInput
- En el
input
, en el atributoref
, agregamos la variabletextInput
, con esto, le estamos indicando a React qué debe hacer referencia a esenodo DOM
. - Cuando le damos click al botón, se ejecuta la función
focusTextInput
, donde usamos la referencia delnodo DOM
; y mediante la propiedad.current
hacemos focus al elemento.
Podemos observar que cada vez que le damos click al botón, nos pone el focus en el input
.
:: Mantener una variable mutable
Tenemos dos formas de mantener el valor de una variable:
- Variable de estado — Con
useState
ouseReducer
, estas variables, siempre que se actualizan provocan un nuevo renderizado del componente. - Por referencia: Por medio de
useRef
, mediante la propiedad.current
, la mutación de esta no causará un nuevo renderizado del componente.
Veamos un ejemplo usando useRef
:
Cada vez que hacemos click en el botón se va a incrementar el valor de counter
(no debemos olvidar que su valor está en la propiedad .current
)
Importante :
useRef
NO NOTIFICA cuando su contenido cambia. Mutar la propiedad.current
no causa otro renderizado.
:: Ventajas y desventajas
useRef
crea un objeto JavaScript plano y la ventaja de usarlo es que SIEMPRE nos va a dar el mismo objeto de referencia en cada renderizado.- Ayuda con el flujo de datos unidireccional (dirección única), esto quiere decir que podemos definir una referencia de un
nodo
primario y arrojarlo a componentes secundarios (de ahí el unidireccional). - Una referencia creada con
useRef
se creará solo cuando el componente se haya montado y preservado durante todo el ciclo de vida. - Actualizar una referencia es un efecto secundario, por lo que debe hacerse solo dentro de un
useEffect
(ouseLayoutEffect
) o dentro de un controlador de eventos.
Sólo hay 3 buenas razones para usar useRef
:
- Administrar el focus, la selección de texto o la reproducción de medios.
- Activar animaciones imperativas.
- Integración de bibliotecas
DOM
de terceros.
Si no es ninguna de las anteriores evitar usarlo.
iii. Customs Hook
Construir tus propios Hooks te permite extraer la lógica del componente en funciones reutilizables.[ref]
Los Hooks personalizados, básicamente son funciones de JavaScript cuyo nombre tiene el prefijo use
(es recomendable que inicien con ese nombre, con la finalidad de indicarle a React que es un Hook, y pueda aplicar las reglas de los Hooks que ya conocemos).
Quizás estés pensando:
— ¿Para qué hacer Hooks personalizados, si puedo tener funciones para reutilizar la funcionalidad? —
… dejame decirte que es una excelente pregunta… la ventaja de usar Hooks personalizados es que se pueden “enganchar” al ciclo de vida y estado del componente, cosa que una función “mortal” no puede…
:: Ejemplo 1
Vamos a crear un Hook bastante sencillo… imagina que quieres mostrarle al usuario, cuando se encuentra en conectado o desconectado de la página.
Entonces, debemos de validar si tiene internet o no (En caso de tener le decimos que está conectado, caso contrario desconectado).
La idea es así:
Entonces, vamos a crear un Hook Hooks/useOnlineStatus.jsx
:
El navegador tiene dos eventos que podemos escuchar para saber si tiene conexión o no el usuario[ref]: online
y offline
, al ser un efecto secundario, vamos a utilizar useEffect
:
Donde:
- (A) — Estamos validando si el navegador tiene la opción de agregar un evento.
- (B) — Agregamos dos eventos
online
yoffline
, donde van a ejecutar cada uno un método. - (C) — Cuando se desmonte el Hook, vamos a remover los eventos.
- (D) — Los eventos que van a actualizar el estado
onlineStatus
El Hook completo:
Dependiendo el status en el que se encuentre va a regresar true/false
.
Es momento de utilizar el Hook personalizado, entonces, vamos a App
para importarlo:
Agregamos al componente funcional App
el Hook:
Al regresar una bandera, podemos hacer una simple validación.
:: Ejemplo completo
:: Ejemplo 2
Vamos a crear un Hook para agregar eventos, para poder utilizarlo en otros lados y de paso refactorizar useOnlineStatus
.
Creamos el Hook personalizado Hook/useEventListener.jsx
Donde:
eventName
— Nombre del evento.handler
— Función que va a ejecutar el evento.element
— Puede serwindow
o unnodo DOM
Entonces, hagamos que element
sea opcional:
Ahora vamos a guardar la referencia de la función del evento en un useRef
, en caso de que cambie, no vaya a renderizar el Hook.
Por último hacemos la validación del eventListener
, lo agregamos y al momento de desmontarlo, lo destruimos:
Código completo del Hook useEventListener
:
Es momento de refactorizar el Hook useOnlineStatus
:
La ventaja de usar useEventListener
:
- Ya no tenemos que preocuparnos de hacer la validación de que si es compatible o no.
- Ya no tenemos que preocuparnos en remover el evento cuando se desmonte el componente.
- Limpieza de código.
:: Ejemplo 2.1
Vamos a crear un componente funcional Components/StatusMouseMove
, con la finalidad de poner en pantalla las coordenadas del mouse, pero utilizando el Hook useEventListener
Usamos useCallback
para persistir la instancia de la función, y no se esté creando una nueva, cada vez que se actualiza el estado setCoords
.
Por último lo agregamos en la App
:
:: Ejemplo completo
:: Ver más…
Si quieres ver Hooks personalizados hecho por los desarrolladores[ref] .
En la siguiente entrega vamos a ver React — Creando la app Search Pokémon con create-react-app, estilos con react-jss y Hooks (useState, useEffect, useRef, useReducer, useCallback y custom Hooks)
La entrega pasada vimos React — Renderizando componentes condicionales (if, if/else, ternary, &&, switch, múltiples condiciones)
Bibliografía y links que te puede interesar…