Hooks Personalizados
Hasta el momento hemos conocido el uso de algunos de los hooks que React nos proporciona, como useState
y useEffect
. Sin embargo, cuando necesitemos tener un comportamiento más específico en nuestra aplicación, no siempre será suficiente con los hooks integrados en React. En estos casos, podemos crear nuestros propios hooks personalizados.
Existen varias razones por las que podríamos necesitar crear un hook personalizado:
- Compartir lógica entre componentes.
- Abstraer lógica compleja.
- Reutilizar lógica en diferentes componentes.
- Mejorar la legibilidad del código.
Cuando identifiquemos que un componente tiene alguna de estas necesidades, es probable que sea un buen momento para crear un hook personalizado.
Creando un hook personalizado
Recordemos que un hook es una función que empieza con la palabra use
. Este puede utilizar otros hooks de ser necesario y devolver un valor que puede ser utilizado en un componente funcional.
Entonces, para dar un ejemplo de cómo crear un hook personalizado, planteamos la siguiente situación: “Contamos con un componente que tiene un estado que representa el tema de la aplicación. Este estado puede ser modificado por el usuario mediante un evento dentro del componente.”
1import React, { useState } from "react";2
3function ThemeChanger() {4 const [theme, setTheme] = useState("light");5
6 const toggleTheme = () => {7 setTheme(theme === "light" ? "dark" : "light");8 };9
10 return (11 <div12 className={`section ${13 theme === "light"14 ? "has-background-light has-text-black"15 : "has-background-black has-text-light"16 }`}17 >18 <h2 className="">Tema Actual: {theme}</h2>19 <button20 className={`button ${21 theme === "light" ? "is-dark" : "is-primary"22 }`}23 onClick={toggleTheme}24 >25 Cambiar Tema26 </button>27 </div>28 );29}
Como vemos una parte de la lógica de este componente podría ser reutilizada. Cuando el estado del tema cambia, el componente se re-renderiza y se actualiza la clase CSS del componente. Entonces, buscaremos definir un hook personalizado llamado useTheme
que se encargará de manejar el estado del tema de la aplicación que tenga un comportamiento similar al componente anterior.
1import { useState } from "react";2
3const useTheme = () => {4 const [theme, setTheme] = useState("light");5
6 const toggleTheme = () => {7 setTheme(theme === "light" ? "dark" : "light");8 };9
10 return [theme, toggleTheme];11};12
13export default useTheme;
En este hook, utilizamos el hook useState
para manejar el estado del tema de la aplicación. La función toggleTheme
se encarga de cambiar el tema actual entre light
y dark
. Finalmente, el hook devuelve un objeto con el estado actual del tema y la función para cambiarlo ([theme, toggleTheme]
), de manera similar a como lo hace useState
sólo que en este caso tenemos un comportamiento especializado.
Utilizando un hook personalizado
Para utilizar el hook personalizado useTheme
en un componente, simplemente importamos la función y la llamamos dentro del componente.
1import useTheme from "../hooks/useTheme";2
3function ThemeChanger() {4 const [theme, toggleTheme] = useTheme();5
6 return (7 <div8 className={`section ${9 theme === "light"10 ? "has-background-light has-text-black"11 : "has-background-black has-text-light"12 }`}13 >14 <h2>Tema Actual: {theme}</h2>15 <button16 className={`button ${17 theme === "light" ? "is-dark" : "is-primary"18 }`}19 onClick={toggleTheme}20 >21 Cambiar Tema22 </button>23 </div>24 );25}
Si bien es un ejemplo sencillo, podemos ver cómo el hook personalizado useTheme
nos permite reutilizar la lógica de cambio de tema en diferentes componentes de nuestra aplicación. Además, al abstraer la lógica de cambio de tema en un hook personalizado, mejoramos la legibilidad del código y mantenemos una estructura más limpia en nuestros componentes.
Hook useFetch
Como mencionamos anteriormente, existen muchas situaciones en las que podemos crear hooks personalizados, todo dependerá de las necesidades específicas de nuestra aplicación. En particular, queremos brindar un ejemplo de un hook personalizado llamado useFetch
que nos permitirá realizar peticiones HTTP de manera sencilla debido a que es una tarea común en aplicaciones web.
Para realizar una petición HTTP será impresindible indicar la ruta a la que se realizará la petición y, opcionalmente, una serie de opciones que permitan configurar la petición (como el método HTTP, el cuerpo de la petición, las cabeceras, etc.). Por lo tanto, el hook useFetch
deberá recibir como argumento dichos elementos.
1function useFetch = (url, options = {}) {2 ...3}
Por otro lado, recordemos que cualquier petición HTTP en JavaScript realizada mediante fetch
es una operación asíncrona que devuelve una promesa. Por lo tanto, nuestro hook personalizado useFetch
deberá administrar el estado de la petición y devolver los datos obtenidos.
- El hook deberá contar con un estado que represente los datos obtenidos de la petición. Para esto, utilizaremos el hook
useState
y lo inicializaremos con un valornull
. - El hook deberá contar con un estado que indique si la petición se encuentra en curso, es decir, si se está pendiente de la respuesta. Para esto, utilizaremos el hook
useState
y lo inicializaremos con un valortrue
. - El hook deberá contar con un estado que indique si la petición ha finalizado correctamente. Para esto, utilizaremos el hook
useState
y lo inicializaremos con un valorfalse
.
1const [data, setData] = useState(null);2const [isError, setIsError] = useState(false);3const [isLoading, setIsLoading] = useState(true);
Una vez más, debido a la naturaleza asíncrona de las peticiones HTTP, necesitaremos esperar a que la petición se complete para obtener los datos. Esto implicará que el primer renderizado del componente que implemente nuestro hook personalizado useFetch
no contará con los datos de la petición. Por lo tanto, emplearemos el hook useEffect
para sincronizar el estado de la petición con los datos obtenidos una vez se complete la petición a la URL indicada.
1useEffect(() => {2 setData(undefined);3 setIsError(false);4 setIsLoading(true);5
6 fetch(url, { ...options })7 .then((res) => {8 if (res.ok) {9 return res.json();10 } else {11 throw new Error("Error en la petición");12 }13 })14 .then(setData)15 .catch((e) => {16 setIsError(true);17 })18 .finally(() => {19 setIsLoading(false);20 });21}, [url]);
Como vemos, los métodos then
, catch
y finally
nos permiten gestionar el flujo de la petición HTTP y, por consiguiente, actualizar los estados de la petición.
- Si la petición se completa, tendremos dos posible escenarios:
- Si la petición es exitosa (
res.ok
), convertimos la respuesta a formato JSON y actualizamos el estadodata
con los datos obtenidos. - Si la petición falla, se lanza un error para ser capturado por el método
catch
.
- Si la petición es exitosa (
- Si la petición falla, actualizamos el estado
isError
atrue
. - Finalmente, independientemente del resultado de la petición, actualizamos el estado
isLoading
afalse
.
Resaltamos las primeras sentencias dentro del hook useEffect
que se encargan de reiniciar los estados de la petición. Esto es necesario para que, en caso de que se realice una nueva petición, los estados de seguimiento de la petición se reinicien también.
Así, nuestro hook personalizado useFetch
sólo deberá devolver los estados de la petición para que puedan ser utilizados en el componente que lo implemente.
1import { useEffect, useState } from "react";2
3export function useFetch(url, options = {}) {4 const [data, setData] = useState();5 const [isError, setIsError] = useState(false);6 const [isLoading, setIsLoading] = useState(true);7
8 useEffect(() => {9 setData(undefined);10 setIsError(false);11 setIsLoading(true);12
13 fetch(url, { ...options })14 .then((res) => {15 if (res.ok) {16 return res.json();17 } else {18 throw new Error("Error en la petición");19 }20 })21 .then(setData)22 .catch((e) => {23 setIsError(true);24 })25 .finally(() => {26 setIsLoading(false);27 });28 }, [url]);29
30 return { data, isError, isLoading };31}32
33export default useFetch;
Recomendaciones
- Nombres descriptivos: al igual que con los componentes, es importante que los hooks personalizados tengan nombres descriptivos que indiquen su funcionalidad.
- Documentación: al igual que con cualquier función, es importante documentar los hooks personalizados para que otros desarrolladores puedan entender su funcionamiento.
- Reutilización: los hooks personalizados nos permiten reutilizar lógica en diferentes componentes. Por lo tanto, es importante identificar patrones de lógica que puedan ser abstraídos en un hook personalizado. También existen buenas prácticas para la creación de hooks personalizados que pueden ser útiles.
-
Definirlo en un archivo separado y exportarlo para que pueda ser importado en cualquier componente que lo necesite. El archivo que contiene el hook personalizado puede tener cualquier nombre, pero es común que se utilice el mismo nombre de la función que define el hook.
-
No necesariamente debe estar en la misma carpeta que los componentes, puede definirse en una carpeta
hooks
en el proyecto. -
A diferencia de los componentes, los hooks personalizados no suelen requerir elementos JSX, por lo que no es necesario que los implementemos en un archivo con dicha extensión, pueden ser implementados en archivos con extensión
.js
.
-