Saltearse al contenido

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.”

1
import React, { useState } from "react";
2
3
function ThemeChanger() {
4
const [theme, setTheme] = useState("light");
5
6
const toggleTheme = () => {
7
setTheme(theme === "light" ? "dark" : "light");
8
};
9
10
return (
11
<div
12
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
<button
20
className={`button ${
21
theme === "light" ? "is-dark" : "is-primary"
22
}`}
23
onClick={toggleTheme}
24
>
25
Cambiar Tema
26
</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.

1
import { useState } from "react";
2
3
const 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
13
export 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.

1
import useTheme from "../hooks/useTheme";
2
3
function ThemeChanger() {
4
const [theme, toggleTheme] = useTheme();
5
6
return (
7
<div
8
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
<button
16
className={`button ${
17
theme === "light" ? "is-dark" : "is-primary"
18
}`}
19
onClick={toggleTheme}
20
>
21
Cambiar Tema
22
</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.

1
function 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 valor null.
  • 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 valor true.
  • 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 valor false.
1
const [data, setData] = useState(null);
2
const [isError, setIsError] = useState(false);
3
const [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.

1
useEffect(() => {
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 estado data 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 falla, actualizamos el estado isError a true.
  • Finalmente, independientemente del resultado de la petición, actualizamos el estado isLoading a false.

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.

1
import { useEffect, useState } from "react";
2
3
export 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
33
export 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.

Bibliografía