Custom Hooks: Reutilizar lógica en React

Los custom hooks son una de las características más poderosas de React. Te permiten extraer lógica de componentes y reutilizarla en múltiples lugares de tu aplicación, mejorando la organización, mantenibilidad y composición del código.

En esta clase aprenderás qué son los custom hooks, cómo crearlos, cuándo usarlos y verás un ejemplo práctico transformando un router casero en un hook reutilizable.

¿Qué es un custom hook?

Un custom hook es una función de JavaScript que:

  1. Su nombre empieza con use (convención obligatoria)
  2. Puede usar otros hooks de React dentro
  3. Encapsula lógica que quieres reutilizar
  4. Retorna valores que otros componentes pueden usar
// Ejemplo básico de un custom hook
function useWindowWidth() {
  const [width, setWidth] = useState(window.innerWidth)

  useEffect(() => {
    const handleResize = () => setWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return width
}

¿Por qué crear custom hooks?

Antes de ver cómo crearlos, entendamos por qué son importantes.

Problema: Lógica duplicada

Imagina que tienes varios componentes que necesitan detectar el ancho de la ventana:

function Header() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth)

  useEffect(() => {
    const handleResize = () => setWindowWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return <header>{windowWidth < 768 ? 'Móvil' : 'Escritorio'}</header>
}

function Sidebar() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth)

  useEffect(() => {
    const handleResize = () => setWindowWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return windowWidth < 768 ? null : <aside>Sidebar</aside>
}

Problemas:

  • Código duplicado en múltiples componentes
  • Si necesitas cambiar la lógica, debes actualizarla en todos lados
  • Los componentes mezclan UI y lógica de estado
  • Difícil de testear la lógica de forma aislada

Solución: Custom hook

// hooks/useWindowWidth.js
function useWindowWidth() {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth)

  useEffect(() => {
    const handleResize = () => setWindowWidth(window.innerWidth)
    window.addEventListener('resize', handleResize)
    return () => window.removeEventListener('resize', handleResize)
  }, [])

  return windowWidth
}

// Componentes
function Header() {
  const windowWidth = useWindowWidth()
  return <header>{windowWidth < 768 ? 'Móvil' : 'Escritorio'}</header>
}

function Sidebar() {
  const windowWidth = useWindowWidth()
  return windowWidth < 768 ? null : <aside>Sidebar</aside>
}

Ventajas:

  • Reutilización: La lógica está en un solo lugar
  • 🧹 Separación de responsabilidades: Componentes se enfocan en UI
  • 🔧 Mantenibilidad: Cambios en un solo lugar
  • 🧪 Testing: Puedes testear el hook de forma aislada
  • 🔄 Composición: Un hook puede usar otros hooks

Ejemplo práctico: Creando useRouter

Vamos a transformar la lógica de un router casero en un custom hook reutilizable, siguiendo el ejemplo del vídeo.

Antes: Componente con lógica mezclada

function App() {
  const [currentPath, setCurrentPath] = useState(window.location.pathname)

  useEffect(() => {
    const handleLocationChange = () => {
      setCurrentPath(window.location.pathname)
    }

    window.addEventListener('popstate', handleLocationChange)

    return () => {
      window.removeEventListener('popstate', handleLocationChange)
    }
  }, [])

  const navigateTo = (path) => {
    window.history.pushState({}, '', path)
    setCurrentPath(path)
  }

  return (
    <div>
      <nav>
        <button onClick={() => navigateTo('/')}>Home</button>
        <button onClick={() => navigateTo('/about')}>About</button>
        <button onClick={() => navigateTo('/contact')}>Contact</button>
      </nav>

      <main>
        {currentPath === '/' && <Home />}
        {currentPath === '/about' && <About />}
        {currentPath === '/contact' && <Contact />}
      </main>
    </div>
  )
}

Problemas:

  • El componente App mezcla lógica de routing con UI
  • No podemos reutilizar esta lógica en otros componentes
  • Difícil de mantener y testear

Paso 1: Extraer la lógica a un custom hook

Vamos a crear un archivo hooks/useRouter.js:

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

export function useRouter() {
  // Estado para la ruta actual
  const [currentPath, setCurrentPath] = useState(window.location.pathname)

  // Efecto para detectar cambios de navegación
  useEffect(() => {
    const handleLocationChange = () => {
      setCurrentPath(window.location.pathname)
    }

    // Escuchar cambios en el historial (botón atrás/adelante)
    window.addEventListener('popstate', handleLocationChange)

    // Cleanup: remover el listener al desmontar
    return () => {
      window.removeEventListener('popstate', handleLocationChange)
    }
  }, [])

  // Función para navegar programáticamente
  const navigateTo = (path) => {
    window.history.pushState({}, '', path)
    setCurrentPath(path)
  }

  // Retornar la API del hook
  return {
    currentPath,
    navigateTo,
  }
}

Paso 2: Usar el hook en el componente

Ahora el componente App queda mucho más limpio:

import { useRouter } from './hooks/useRouter'

function App() {
  const { currentPath, navigateTo } = useRouter()

  return (
    <div>
      <nav>
        <button onClick={() => navigateTo('/')}>Home</button>
        <button onClick={() => navigateTo('/about')}>About</button>
        <button onClick={() => navigateTo('/contact')}>Contact</button>
      </nav>

      <main>
        {currentPath === '/' && <Home />}
        {currentPath === '/about' && <About />}
        {currentPath === '/contact' && <Contact />}
      </main>
    </div>
  )
}

¡Mucho mejor! El componente ahora solo se preocupa de la UI.

Paso 3: Reutilizar el hook en otros componentes

Ahora podemos usar el mismo hook en cualquier componente que necesite información del router:

function Navigation() {
  const { currentPath, navigateTo } = useRouter()

  return (
    <nav>
      <button className={currentPath === '/' ? 'active' : ''} onClick={() => navigateTo('/')}>
        Home
      </button>
      <button
        className={currentPath === '/about' ? 'active' : ''}
        onClick={() => navigateTo('/about')}
      >
        About
      </button>
    </nav>
  )
}

function Breadcrumbs() {
  const { currentPath } = useRouter()

  return (
    <div>
      Estás en: <strong>{currentPath}</strong>
    </div>
  )
}

Anatomía de un custom hook

Un custom hook típico tiene esta estructura:

import { useState, useEffect } from 'react'

export function useMyCustomHook(parameters) {
  // 1. Estado interno del hook
  const [state, setState] = useState(initialValue)

  // 2. Efectos secundarios
  useEffect(() => {
    // Lógica del efecto
    return () => {
      // Cleanup
    }
  }, [dependencies])

  // 3. Funciones auxiliares
  const helperFunction = () => {
    // Lógica
  }

  // 4. Retornar la API del hook
  return {
    state,
    helperFunction,
  }
}

Más ejemplos de custom hooks

1. useLocalStorage

Un hook para sincronizar estado con localStorage:

import { useState, useEffect } from 'react'

export function useLocalStorage(key, initialValue) {
  // Obtener valor inicial del localStorage
  const [value, setValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch (error) {
      console.error(error)
      return initialValue
    }
  })

  // Actualizar localStorage cuando cambia el valor
  useEffect(() => {
    try {
      window.localStorage.setItem(key, JSON.stringify(value))
    } catch (error) {
      console.error(error)
    }
  }, [key, value])

  return [value, setValue]
}

Uso:

function App() {
  const [name, setName] = useLocalStorage('userName', 'Anónimo')

  return (
    <div>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <p>Hola, {name}!</p>
    </div>
  )
}

2. useFetch

Un hook para hacer peticiones HTTP:

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(() => {
    setLoading(true)
    setError(null)

    fetch(url)
      .then((response) => {
        if (!response.ok) throw new Error('Error en la petición')
        return response.json()
      })
      .then((data) => {
        setData(data)
        setLoading(false)
      })
      .catch((error) => {
        setError(error.message)
        setLoading(false)
      })
  }, [url])

  return { data, loading, error }
}

Uso:

function UserProfile({ userId }) {
  const { data: user, loading, error } = useFetch(`/api/users/${userId}`)

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

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  )
}

3. useToggle

Un hook simple para manejar valores booleanos:

import { useState } from 'react'

export function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue)

  const toggle = () => setValue((prev) => !prev)
  const setTrue = () => setValue(true)
  const setFalse = () => setValue(false)

  return {
    value,
    toggle,
    setTrue,
    setFalse,
  }
}

Uso:

function Modal() {
  const { value: isOpen, toggle, setFalse } = useToggle(false)

  return (
    <div>
      <button onClick={toggle}>Abrir Modal</button>

      {isOpen && (
        <div className="modal">
          <h2>Modal abierto</h2>
          <button onClick={setFalse}>Cerrar</button>
        </div>
      )}
    </div>
  )
}

Composición de hooks

Una de las mayores ventajas de los custom hooks es que pueden usar otros hooks, incluidos otros custom hooks:

// Hook base: useLocalStorage
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    const item = window.localStorage.getItem(key)
    return item ? JSON.parse(item) : initialValue
  })

  useEffect(() => {
    window.localStorage.setItem(key, JSON.stringify(value))
  }, [key, value])

  return [value, setValue]
}

// Hook compuesto: useDarkMode (usa useLocalStorage)
function useDarkMode() {
  const [isDark, setIsDark] = useLocalStorage('darkMode', false)

  useEffect(() => {
    if (isDark) {
      document.body.classList.add('dark')
    } else {
      document.body.classList.remove('dark')
    }
  }, [isDark])

  const toggle = () => setIsDark((prev) => !prev)

  return { isDark, toggle }
}

Uso:

function App() {
  const { isDark, toggle } = useDarkMode()

  return (
    <div>
      <button onClick={toggle}>{isDark ? '☀️ Modo claro' : '🌙 Modo oscuro'}</button>
      <p>El tema actual es: {isDark ? 'oscuro' : 'claro'}</p>
    </div>
  )
}

Mejorando el useRouter con más funcionalidades

Vamos a mejorar nuestro useRouter inicial añadiendo más funcionalidades:

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

export function useRouter() {
  const [currentPath, setCurrentPath] = useState(window.location.pathname)

  useEffect(() => {
    const handleLocationChange = () => {
      setCurrentPath(window.location.pathname)
    }

    window.addEventListener('popstate', handleLocationChange)
    return () => window.removeEventListener('popstate', handleLocationChange)
  }, [])

  const navigateTo = (path) => {
    window.history.pushState({}, '', path)
    setCurrentPath(path)
  }

  const goBack = () => {
    window.history.back()
  }

  const goForward = () => {
    window.history.forward()
  }

  // Helper para verificar si estamos en una ruta específica
  const isActive = (path) => currentPath === path

  // Helper para obtener parámetros de query string
  const getQueryParams = () => {
    const params = new URLSearchParams(window.location.search)
    return Object.fromEntries(params.entries())
  }

  return {
    currentPath,
    navigateTo,
    goBack,
    goForward,
    isActive,
    queryParams: getQueryParams(),
  }
}

Uso mejorado:

function Navigation() {
  const { navigateTo, isActive } = useRouter()

  return (
    <nav>
      <button className={isActive('/') ? 'active' : ''} onClick={() => navigateTo('/')}>
        Home
      </button>
      <button className={isActive('/about') ? 'active' : ''} onClick={() => navigateTo('/about')}>
        About
      </button>
    </nav>
  )
}

function SearchPage() {
  const { queryParams } = useRouter()

  return (
    <div>
      <h2>Resultados de búsqueda</h2>
      <p>Buscando: {queryParams.q || 'nada'}</p>
    </div>
  )
}

Reglas de los custom hooks

1. El nombre DEBE empezar con “use”

// ✅ Bien
function useCounter() {}
function useAuth() {}
function useApi() {}

// ❌ Mal
function counter() {}
function getAuth() {}
function apiHook() {}

¿Por qué? React necesita identificar los hooks para aplicar sus reglas internas.

2. Solo llamar hooks en el nivel superior

No puedes llamar hooks dentro de condiciones, loops o funciones anidadas:

// ❌ Mal
function MyComponent() {
  if (condition) {
    const value = useMyHook() // ¡Error!
  }

  for (let i = 0; i < 10; i++) {
    useEffect(() => {}) // ¡Error!
  }
}

// ✅ Bien
function MyComponent() {
  const value = useMyHook()

  useEffect(() => {
    if (condition) {
      // Lógica condicional dentro del efecto
    }
  }, [condition])
}

3. Solo llamar hooks desde componentes o custom hooks

// ✅ Bien: Desde un componente
function MyComponent() {
  const value = useMyHook()
}

// ✅ Bien: Desde otro custom hook
function useCustomHook() {
  const value = useMyHook()
}

// ❌ Mal: Desde una función normal
function helperFunction() {
  const value = useMyHook() // ¡Error!
}

Cuándo crear un custom hook

Crea un custom hook cuando:

Tienes lógica duplicada en múltiples componentes ✅ La lógica usa hooks de React ✅ Quieres separar lógica de UI para mejor organización ✅ Necesitas testear la lógica de forma aislada ✅ Quieres compartir lógica entre componentes

NO crees un custom hook si:

❌ Solo usas la lógica en un componente ❌ La lógica es muy simple (una línea) ❌ No usa ningún hook de React ❌ Es solo una función helper normal

Estructura de carpetas recomendada

src/
├── components/
│   ├── Header.jsx
│   ├── Sidebar.jsx
│   └── Navigation.jsx
├── hooks/
│   ├── useRouter.js
│   ├── useLocalStorage.js
│   ├── useFetch.js
│   ├── useDebounce.js
│   └── useToggle.js
├── pages/
│   ├── Home.jsx
│   ├── About.jsx
│   └── Contact.jsx
└── App.jsx

Ventajas de los custom hooks

VentajaDescripción
🔄 ReutilizaciónUsa la misma lógica en múltiples componentes
🧹 SeparaciónSepara lógica de UI para mejor organización
🔧 MantenibilidadCambios en un solo lugar
🧪 TestingTestea la lógica de forma aislada
🎯 ComposiciónCombina múltiples hooks para crear funcionalidad compleja
📖 LegibilidadCódigo más limpio y fácil de entender

Conclusión

Los custom hooks son una herramienta fundamental en React moderno:

  • 🎯 Extraen lógica reutilizable de los componentes
  • 🧩 Mejoran la composición del código
  • 🧹 Separan responsabilidades entre UI y lógica
  • 🔧 Facilitan el mantenimiento al centralizar la lógica
  • 🧪 Permiten testing de forma aislada

Empieza identificando patrones repetidos en tu código y extráelos a custom hooks. Con el tiempo, construirás una librería de hooks reutilizables que acelerarán tu desarrollo.