Hooks Personalizados
Extrae lógica con estado reutilizable en funciones que comienzan con use - el mecanismo principal de reutilización de código de React.
Busca en todas las páginas de la documentación
Extrae lógica con estado reutilizable en funciones que comienzan con use - el mecanismo principal de reutilización de código de React.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tarjeta de referencia rápida - lista para copiar y pegar.
// Pattern: a custom hook is just a function that calls other hooks
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue((v) => !v), []);
return [value, toggle] as const;
}
// Usage
const [isOpen, toggleOpen] = useToggle(false);Cuándo usarlo: Cuando encuentras que estás duplicando la misma combinación de useState, useEffect, useRef u otros hooks en varios componentes.
"use client";
import { useCallback, useEffect, useState } from "react";
// Custom hook: local storage state
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === "undefined") return initialValue;
try {
const stored = localStorage.getItem(key);
return stored ? (JSON.parse(stored) as T) : initialValue;
} catch {
return initialValue;
}
});
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch {
// Storage full or unavailable
}
}, [key, value]);
const remove = useCallback(() => {
setValue(initialValue);
localStorage.removeItem(key);
}, [key, initialValue]);
return [value, setValue, remove] as const;
}
// Component using the custom hook
export function Preferences() {
const [name, setName, clearName] = useLocalStorage("user-name", "");
const [darkMode, setDarkMode] = useLocalStorage("dark-mode", false);
return (
<div className={`space-y-4 p-4 rounded ${darkMode ? "bg-gray-900 text-white" : "bg-white"}`}>
<div>
<label className="block text-sm font-medium mb-1">Nombre</label>
<input
value={name}
onChange={(e) => setName(e.target.value)}
className="border rounded px-3 py-2 text-black"
placeholder="Ingresa tu nombre"
/>
</div>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={darkMode}
onChange={(e) => setDarkMode(e.target.checked)}
/>
<span className="text-sm">Modo oscuro</span>
</label>
<div className="flex gap-2">
<button onClick={clearName} className="text-sm text-blue-500 underline">
Limpiar nombre
</button>
</div>
{name && <p className="text-sm">¡Hola, {name}!</p>}
</div>
);
}Lo que esto demuestra:
useLocalStorage) que compone useState, useEffect y useCallbacktypeof windowas const estrecha el tipo de retorno de Array a una tupla específicause y llama otros hooksuse es obligatorio - le señala a React (y a las herramientas de linting) que la función sigue las reglas de hooks| Patrón | Convención | Ejemplo |
|---|---|---|
| Valor único | Devuelve el valor directamente | useOnlineStatus() → boolean |
| Valor + setter | Devuelve una tupla [value, setter] | useToggle() → [boolean, () => void] |
| Múltiples valores | Devuelve un objeto | useFetch() → { data, error, loading } |
| Solo acciones | Devuelve un objeto de funciones | useClipboard() → { copy, paste } |
useDebounce - deduplica un valor que cambia rápidamente:
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
// Usage
const debouncedQuery = useDebounce(query, 300);useFetch - obtención de datos con estados de carga y error:
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<Error | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
fetch(url, { signal: controller.signal })
.then((res) => res.json())
.then((json) => { setData(json); setError(null); })
.catch((err) => { if (err.name !== "AbortError") setError(err); })
.finally(() => setLoading(false));
return () => controller.abort();
}, [url]);
return { data, error, loading };
}useMediaQuery - puntos de quiebre responsivos:
function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const mql = window.matchMedia(query);
setMatches(mql.matches);
function handler(e: MediaQueryListEvent) {
setMatches(e.matches);
}
mql.addEventListener("change", handler);
return () => mql.removeEventListener("change", handler);
}, [query]);
return matches;
}
// Usage
const isMobile = useMediaQuery("(max-width: 768px)");useClickOutside - detecta clics fuera de un ref:
function useClickOutside(ref: RefObject<HTMLElement>, handler: () => void) {
useEffect(() => {
function handleClick(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
handler();
}
}
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, [ref, handler]);
}usePrevious - rastrea el valor anterior:
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
});
return ref.current;
}// Use generics for reusable hooks
function useLocalStorage<T>(key: string, initial: T): [T, (v: T) => void] { ... }
// Use `as const` for tuple returns so destructuring types are correct
function useToggle(initial = false) {
const [value, setValue] = useState(initial);
const toggle = useCallback(() => setValue(v => !v), []);
return [value, toggle] as const;
// Return type: readonly [boolean, () => void]
// Without `as const`: (boolean | (() => void))[]
}
// Use overloads for hooks with multiple call signatures
function useControllable<T>(value: T): [T, (v: T) => void];
function useControllable<T>(value: undefined, defaultValue: T): [T, (v: T) => void];
function useControllable<T>(value: T | undefined, defaultValue?: T) {
const [internal, setInternal] = useState(defaultValue ?? value!);
if (value !== undefined) return [value, () => {}] as const;
return [internal, setInternal] as const;
}No comenzar con use - Si tu hook se llama getToggle en lugar de useToggle, el linter no aplicará las reglas de hooks, generando bugs sutiles. Solución: Siempre prefija los hooks personalizados con use.
Llamar hooks condicionalmente dentro de hooks personalizados - Las reglas de hooks se aplican también dentro de hooks personalizados. Solución: Nunca pongas useState o useEffect dentro de un bloque if o después de un retorno temprano.
Devolver referencias inestables - Devolver un nuevo objeto { value, toggle } en cada renderizado causa que las dependencias de useEffect de los consumidores cambien cada vez. Solución: Usa useMemo para estabilizar objetos, o devuelve una tupla.
Sobre-abstraer - Crear un hook personalizado para lógica usada solo en un componente agrega indirección sin beneficio. Solución: Extrae en un hook personalizado solo cuando la lógica se usa en 2+ componentes o cuando mejora la legibilidad de un componente complejo.
Limpieza faltante - Olvidar limpiar suscripciones, temporizadores o listeners de eventos en tu hook personalizado causa fugas de memoria. Solución: Siempre devuelve una función de limpieza desde useEffect dentro de tu hook.
Cierre obsoleto en callbacks devueltos - Las callbacks devueltas desde hooks personalizados pueden quedar atrapadas en estado obsoleto si no se envuelven en useCallback con dependencias apropiadas. Solución: Usa useCallback para cualquier función que devuelvas, o usa el patrón de actualizador.
| Alternativa | Úsalo Cuando | No lo uses cuando |
|---|---|---|
| Render props | Necesitas compartir lógica de renderizado de UI, no solo estado | Solo necesitas compartir lógica con estado |
| Componentes de orden superior (HOC) | La base de código heredada requiere envolver componentes | Empezando código nuevo - los hooks son más simples |
| Funciones de utilidad | La lógica es pura (sin hooks, sin estado, sin efectos) | La lógica involucra estado de React o ciclo de vida |
| Context + Provider | El estado compartido necesita ser accesible por todo el subárbol | Cada consumidor necesita estado independiente |
| Hooks de terceros (react-use, usehooks-ts) | Una implementación bien probada ya existe | Tu caso de uso es único para tu dominio |
Cuándo extraer un hook personalizado: Si tienes 2+ componentes con la misma combinación de useState + useEffect, o si la lógica de hooks de un componente excede ~15 líneas y tiene una responsabilidad clara, extrae la lógica.
De una aplicación SaaS de producción Next.js 15 / React 19 (SystemsArchitect.io).
// Production example: Auth hook with session + real-time subscription
// File: src/hooks/use-auth.ts
'use client'
import { useEffect, useState, useMemo, useCallback } from 'react'
import { type User } from '@supabase/supabase-js'
import { supabase } from '@/lib/supabase/client'
export function useAuth() {
const [user, setUser] = useState<User | null>(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const getSession = async () => {
try {
const { data: { session } } = await supabase.auth.getSession()
setUser(session?.user ?? null)
} catch (error) {
console.error('Error getting session:', error)
} finally {
setLoading(false)
}
}
getSession()
const { data: { subscription } } = supabase.auth.onAuthStateChange(
async (event, session) => {
setUser(session?.user ?? null)
setLoading(false)
}
)
return () => { subscription.unsubscribe() }
}, [])
const signOut = useCallback(async () => {
try {
await supabase.auth.signOut()
setUser(null)
} catch (error) {
console.error('Error signing out:', error)
}
}, [])
return useMemo(() => ({
user,
loading,
signOut,
isAuthenticated: user !== null,
userId: user?.id || null,
}), [user, loading, signOut])
}Lo que esto demuestra en producción:
subscription.unsubscribe() previene fugas de memoria cuando el componente se desmontauseCallback en signOut crea una referencia estable para que los consumidores que la usan en arrays de dependencias no vuelvan a ejecutar efectosuseMemo en el objeto de retorno previene re-renderizados innecesarios. Sin él, se crea una nueva referencia de objeto en cada renderizado incluso si los valores son igualesisAuthenticated: user !== null es un valor derivado calculado del estado, no almacenado por separadouseAuth() obtiene su propio estado pero todos se sincronizan a través de onAuthStateChangeuse y llama otros hooks (useState, useEffect, etc.) internamente.use.// Without `as const`: (boolean | (() => void))[]
// With `as const`: readonly [boolean, () => void]
return [value, toggle] as const;as const estrecha el tipo de retorno a una tupla específica, habilitando tipos de desestructuración correctos.use.function useLocalStorage<T>(key: string, initial: T) {
const [value, setValue] = useState<T>(() => {
if (typeof window === "undefined") return initial;
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initial;
});
// ...
}typeof window === "undefined" o mantenlo dentro de useEffect.{ value, toggle } crea una nueva referencia de objeto en cada renderizado.useEffect, el efecto se vuelve a ejecutar en cada renderizado.useMemo o devolviendo una tupla en su lugar.function useLocalStorage<T>(key: string, initial: T): [T, (v: T) => void] {
const [value, setValue] = useState<T>(() => {
if (typeof window === "undefined") return initial;
const stored = localStorage.getItem(key);
return stored ? (JSON.parse(stored) as T) : initial;
});
// ...
return [value, setValue];
}<T> para hacer el hook reutilizable con cualquier tipo de dato.useCallback para proporcionar referencias estables.useCallback, los consumidores que usan la función en arrays de dependencias obtienen una nueva referencia en cada renderizado.setState(prev => ...)) dentro de useCallback para minimizar dependencias.renderHook desde @testing-library/react para renderizar el hook sin un componente.act().useAuth puede llamar useLocalStorage, que llama useState y useEffect.[value, setter]) permiten a los consumidores renombrar las variables: const [name, setName] = useLocalStorage(...).{ data, error, loading }) son mejores cuando hay muchos valores de retorno y el orden no importa.Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥