DEV Community

Cover image for 🔹 Custom Hooks – Reusar lógica (7/8)
Pedro Alvarado
Pedro Alvarado

Posted on

🔹 Custom Hooks – Reusar lógica (7/8)

📋 Tabla de contenidos

  1. useState – El fundamento del estado
  2. useEffect – Efectos secundarios y ciclo de vida
  3. useContext – Compartir estado sin props
  4. useReducer – Lógica de Estado Compleja y predecible
  5. useRef – Referencias, Almacenamiento y Escape
  6. useMemo y useCallback – Optimizando el Rendimiento
  7. Custom Hooks – Creando Lógica Reutilizable
  8. Errores Comunes y Soluciones

¿Qué son y por qué son tan potentes?

Los Custom Hooks (Hooks Personalizados) son la característica más poderosa de los Hooks. Te permiten extraer y reutilizar lógica con estado de un componente. No son un hook nuevo de React, sino una convención: una función JavaScript que sigue dos reglas:

  1. Su nombre debe empezar con use (ej. useFetch, useLocalStorage).
  2. Puede llamar a otros hooks (como useState, useEffect, etc.).

Piensa en ellos como piezas de Lego lógicas. ¿Necesitas saber si el usuario está online? useOnlineStatus. ¿Quieres interactuar con el Local Storage? useLocalStorage. ¿Necesitas hacer fetching de datos en varios sitios? useFetch.

Creando nuestro primer Custom Hook: useFetch

El fetching de datos es un caso de uso perfecto. Casi siempre necesitas manejar el estado de carga, los posibles errores y los datos resultantes. Vamos a encapsular toda esa lógica en un hook reutilizable.

El código del hook useFetch.js:

// hooks/useFetch.js
import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // Si la URL no es válida, no hacemos nada.
    if (!url) return;

    // Usamos AbortController para cancelar el fetch si el componente
    // se desmonta o la URL cambia antes de que termine.
    const controller = new AbortController();

    const fetchData = async () => {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(url, { signal: controller.signal });
        if (!response.ok) {
          throw new Error(`Error: ${response.status}`);
        }
        const result = await response.json();
        setData(result);
      } catch (err) {
        if (err.name !== 'AbortError') {
          setError(err.message);
        }
      } finally {
        // Solo cambiamos loading a false si no fue un aborto
        if (!controller.signal.aborted) {
          setLoading(false);
        }
      }
    };

    fetchData();

    // Función de limpieza: aborta el fetch si es necesario.
    return () => {
      controller.abort();
    };
  }, [url]); // Se vuelve a ejecutar si la URL cambia

  // El hook devuelve un objeto con el estado del fetch
  return { data, loading, error };
}
Enter fullscreen mode Exit fullscreen mode

Usando useFetch en un componente

Ahora, consumir esta lógica compleja se vuelve trivialmente simple.

// components/GitHubUser.js
import React, { useState } from 'react';
import { useFetch } from '../hooks/useFetch';

function GitHubUser() {
  const [username, setUsername] = useState('facebook');
  const [input, setInput] = useState('facebook');

  // ¡Toda la lógica de fetch encapsulada en una sola línea!
  const { data, loading, error } = useFetch(`https://api.github.com/users/${username}`);

  const handleSubmit = (e) => {
    e.preventDefault();
    setUsername(input);
  }

  return (
    <div>
      <h3>Buscador de Usuarios de GitHub</h3>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="Introduce un usuario de GitHub"
        />
        <button type="submit">Buscar</button>
      </form>

      {loading && <p>Cargando...</p>}
      {error && <p>Error: {error}</p>}
      {data && (
        <div>
          <h4>{data.name} (@{data.login})</h4>
          <img src={data.avatar_url} alt={data.name} width="100" />
          <p>{data.bio}</p>
        </div>
      )}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Como puedes ver, el componente GitHubUser no sabe nada sobre useState o useEffect. Solo le interesa el resultado final: data, loading y error. Esto hace que el código sea increíblemente limpio, declarativo y fácil de mantener.


✅ Buenas practicas y patrones comunes

  1. Nombres que empiezan con use: Es obligatorio. Esta convención permite a React y a las herramientas de linting saber que tu función es un hook y que debe seguir las reglas de los hooks.
  2. Devuelve un array o un objeto: Si tu hook devuelve múltiples valores, devuélvelos en un objeto { valor1, valor2 } para que el consumo sea más legible. Si es un hook muy genérico y que emula a uno nativo (como useState), puedes devolver un array [valor, actualizador].
  3. Hazlos genéricos y configurables: Un buen custom hook acepta parámetros para modificar su comportamiento. En useFetch, podría aceptar un objeto de opciones para el fetch.
  4. No rompas las reglas de los hooks: Dentro de tu custom hook, sigue aplicando las mismas reglas: no los llames en bucles, condicionales o funciones anidadas.

🚨 Errores comunes y cómo evitarlos

  • Error: Olvidar el prefijo use.
    • Solución: Nombra siempre tus hooks personalizados como useMiHook. Si no, React no podrá verificar que estás siguiendo las reglas de los hooks.
  • Error: Compartir estado entre componentes.
    • Aclaración: Es un error conceptual pensar que un custom hook comparte el mismo estado entre diferentes componentes que lo usan. Cada llamada a un custom hook es completamente independiente y tiene su propio estado interno. Si quieres compartir estado, necesitas useContext o una librería de estado global.

🚀 Retos prácticos

  1. useLocalStorage: Crea un hook que se sincronice con localStorage. Debe funcionar de manera similar a useState, pero persistiendo el valor en el navegador.

    // Uso esperado:
    const [name, setName] = useLocalStorage('username', 'Invitado');
    
  2. useOnlineStatus: Crea un hook que devuelva true si el navegador está conectado a internet y false si no. (Pista: window.addEventListener('online', ...) y window.addEventListener('offline', ...)).

  3. useDebounce: Un hook muy útil. Toma un valor (ej. el texto de un input) y devuelve una versión "retrasada" de ese valor que solo se actualiza después de un cierto tiempo sin cambios. Es perfecto para evitar hacer peticiones a una API en cada pulsación de tecla.

    // Uso esperado:
    const [searchTerm, setSearchTerm] = useState('');
    const debouncedSearchTerm = useDebounce(searchTerm, 500); // 500ms de retraso
    // Usa `debouncedSearchTerm` en tu `useFetch`
    

Top comments (0)