react-custom-hooks

Cómo crear Custom Hooks en React

  • 5 min

Por ahora hemos estado utilizando los Hooks que React nos da “de fábrica”. Por ejemplo, useState para la memoria y useEffect para los efectos.

Pero, ¿Qué ocurre cuando tenemos una lógica compleja que necesitamos usar en varios componentes a la vez?

En React, al igual que extraemos la interfaz repetida a Componentes, a veces extraemos la lógica repetida a Custom Hooks

Un Custom Hook es una función JavaScript que utiliza otros Hooks (como useState, useEffect, etc.) para encapsular lógica reutilizable.

La idea principal es extraer la lógica de un componente y colocarla en una función separada, que luego puede ser utilizada en múltiples componentes.

¿Cuándo crear un Custom Hook?

Los Custom Hooks son una característica muy útil de React. Nos permiten construir nuestro propio “framework” de utilidades adaptado a las necesidades específicas de nuestro proyecto. Pero tampoco os volvais locos.

Mi recomendación es la regla de “Repite dos veces, y luego refactoriza”.

  1. La primera vez, escribid la lógica dentro del componente
  2. La segunda vez que necesitéis lo mismo en otro sitio, copiad y pegad
  3. La tercera vez, es el momento obligatorio de refactorizar y extraer a un Custom Hook

¿Qué es un Custom Hook?

Un Custom Hook es, en realidad, una simple función de JavaScript. Es una función que puede contener lógica, cálculos y, lo más importante, puede llamar a otros Hooks de React en su interior.

Existe una única regla estricta: el nombre de vuestra función DEBE empezar por “use”.

  • useCounter
  • useForm
  • useFetch
  • createCounter

Esto no es solo una convención para que las personas lo leamos mejor. Los Linter de React buscan funciones que empiecen por use para verificar que cumplís las reglas de los hooks (por ejemplo, no llamarlos dentro de condicionales).

Ejemplo 1: Abstraendo estado (useCounter)

Mejor lo vemos con un ejemplo. Vamos a crear nuestro primer Custom Hook. ¡Toca volver a hacer un contador! (ya… sorry por eso 😎)

Como sabemos, el contador es el “Hola Mundo” de la reactividad. Esta vez, queremos encapsular la lógica de un número que sube, baja y se resetea.

Cread un archivo nuevo, por ejemplo /src/hooks/useCounter.js:

import { useState } from "react";

// 1. Definimos la función con prefijo 'use'
export const useCounter = (initialValue = 0) => {
  const [counter, setCounter] = useState(initialValue);

  // 2. Definimos las funciones de manipulación
  const increment = (step = 1) => setCounter(c => c + step);
  const decrement = (step = 1) => setCounter(c => c - step);
  const reset = () => setCounter(initialValue);

  // 3. Devolvemos lo que el componente necesite
  // Puede ser un array [] o un objeto {}
  return {
    counter,
    increment,
    decrement,
    reset,
  };
};
Copied!

¿Cómo lo usamos?

Ahora, en nuestros componentes, el código queda muy limpio. Ya no nos importa cómo se incrementa el contador, solo usamos la funcionalidad.

import { useCounter } from "./hooks/useCounter";

function Carrito() {
  // Inicializamos el hook
  const { counter, increment, decrement } = useCounter(1);

  return (
    <div>
      <h3>Productos: {counter}</h3>
      <button onClick={() => decrement()}>-</button>
      <button onClick={() => increment()}>+</button>
    </div>
  );
}
Copied!

Si mañana queremos cambiar la lógica (por ejemplo, que el contador nunca baje de cero), solo modificamos useCounter.js y todos los componentes se actualizarán.

Compartir lógica vs Compartir estado

Cuando usamos un Custom Hook en dos componentes diferentes, no están compartiendo el estado. Están compartiendo la lógica de estado.

// Componente A
const counterA = useCounter(); // Tiene su propio estado interno (0)

// Componente B
const counterB = useCounter(); // Tiene SU PROPIO estado interno (0)
Copied!

Es decir, que si incremento counterA, el counterB no se entera.

Cada llamada a un Custom Hook crea una instancia totalmente aislada de los estados (useState) y efectos (useEffect) que contenga dentro.

Si lo que queréis es compartir el estado globalmente (que al modificar uno cambien los dos), necesitáis otras herramientas como Context API o gestores de estado global (Zustand, Redux), que veremos más adelante.

Ejemplo 2: Abstraendo efectos (useFetch)

Vamos a ver otro ejemplo mas avanzado, y que no sea un contador. Vamos a encapsular la complejidad de useEffect, peticiones asíncronas y gestión de errores en un hook reutilizable.

El objetivo es que queremos poder pedir datos así de fácil: const { data, loading, error } = useFetch(url);

Así que creamos nuestro fichero src/hooks/useFetch.js con nuestro Custom Hook.

import { useState, useEffect } from "react";

export const useFetch = (url) => {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    // Reseteamos estados al cambiar la URL
    setLoading(true);
    setData(null);
    setError(null);
    
    const controller = new AbortController();

    fetch(url, { signal: controller.signal })
      .then((response) => {
        if (!response.ok) throw new Error("Error en la petición");
        return response.json();
      })
      .then((jsonData) => {
        setData(jsonData);
        setError(null);
      })
      .catch((err) => {
        if (err.name !== "AbortError") {
          setError(err.message);
        }
      })
      .finally(() => {
        setLoading(false);
      });

    // Cleanup: Cancelamos la petición si el componente se desmonta
    return () => controller.abort();
  }, [url]);

  return { data, loading, error };
};
Copied!

Uso en componentes

Fijaos en la diferencia en nuestro componente principal. Hemos pasado de tener 20 líneas de lógica asíncrona a una sola línea declarativa.

function UserList() {
  const { data, loading, error } = useFetch("https://api.example.com/users");

  if (loading) return <p>Cargando...</p>;
  if (error) return <p>Error: {error}</p>;

  return (
    <ul>
      {data.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  );
}
Copied!