React — Hooks [useReducer, useCallback, useMemo, useRef, and customs Hooks](Parte II)

Mauricio Garcia
17 min readNov 3, 2020

--

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 de useState 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.
  • dispatchCounterFunción que utilizaremos para setear el estado, donde le tenemos que mandar el tipo de acción que debe ejecutar.
  • reducerCounterFunción que lanza dispatchCounter, para cambiar el estado de counter.
  • {count:0, frozen:false}Estado inicial de counter.

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 y password .
  • 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 mostrar Form
  • Form — Es el componente donde vamos a escribir un usuario y contraseña, además se encargará de mostrar/ocultar: un mensaje, el componente Loading, 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.
  • dispatchFunción que nos va a servir para ejecutar reducer .
  • reducerFunció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 formulario user y password .
  • 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 valores user y password, 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 mandar SUCCESS o FAILURE
  • 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_DATAEn 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 FAILUREDentro 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 llamada reductor 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 y counter2.
  • 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 contador setCounter1 ó 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 mientras counter1 no cambie, no es necesario que crea una nueva instancia.
  • Le estamos indicando a increment2 que mientras counter2 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 del eslint-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 variable textInput
  • En el input, en el atributo ref, agregamos la variable textInput, con esto, le estamos indicando a React qué debe hacer referencia a ese nodo DOM.
  • Cuando le damos click al botón, se ejecuta la función focusTextInput, donde usamos la referencia del nodo 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 useStateo useReducer, 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(o useLayoutEffect) 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 puedenenganchar” al ciclo de vida y estado del componente, cosa que una funciónmortalno 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 y offline, 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:

  • eventNameNombre del evento.
  • handlerFunción que va a ejecutar el evento.
  • element — Puede ser window o un nodo 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] .

--

--