Aprende a crear custom hooks para extraer y reutilizar lógica entre componentes, mejorando la organización, mantenibilidad y composición de tu código React
midudev
Más clases y directos en YouTube
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:
- Su nombre empieza con
use(convención obligatoria) - Puede usar otros hooks de React dentro
- Encapsula lógica que quieres reutilizar
- Retorna valores que otros componentes pueden usar
// Ejemplo básico de un custom hookfunction 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.jsfunction useWindowWidth() { const [windowWidth, setWindowWidth] = useState(window.innerWidth) useEffect(() => { const handleResize = () => setWindowWidth(window.innerWidth) window.addEventListener('resize', handleResize) return () => window.removeEventListener('resize', handleResize) }, []) return windowWidth}// Componentesfunction 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
Appmezcla 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.jsimport { 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: useLocalStoragefunction 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.jsimport { 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”
// ✅ Bienfunction useCounter() {}function useAuth() {}function useApi() {}// ❌ Malfunction 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:
// ❌ Malfunction MyComponent() { if (condition) { const value = useMyHook() // ¡Error! } for (let i = 0; i < 10; i++) { useEffect(() => {}) // ¡Error! }}// ✅ Bienfunction 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 componentefunction MyComponent() { const value = useMyHook()}// ✅ Bien: Desde otro custom hookfunction useCustomHook() { const value = useMyHook()}// ❌ Mal: Desde una función normalfunction 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
| Ventaja | Descripción |
|---|---|
| 🔄 Reutilización | Usa la misma lógica en múltiples componentes |
| 🧹 Separación | Separa lógica de UI para mejor organización |
| 🔧 Mantenibilidad | Cambios en un solo lugar |
| 🧪 Testing | Testea la lógica de forma aislada |
| 🎯 Composición | Combina múltiples hooks para crear funcionalidad compleja |
| 📖 Legibilidad | Có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.
