React
Custom Hooks
Explica cómo extraer lógica reutilizable en custom hooks, diseñar APIs claras y compartir comportamiento con estado y efectos sin compartir instancias de estado.
- Última actualización
- Actualizada
- Nivel
- Aplicación
React
Explica cómo extraer lógica reutilizable en custom hooks, diseñar APIs claras y compartir comportamiento con estado y efectos sin compartir instancias de estado.
Un custom hook extrae y compone lógica reactiva reutilizable. Comparte comportamiento, no estado: cada llamada conserva su propia instancia de Hooks.
Una función que llama Hooks debe comenzar con use.
function useOnlineStatus() {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}El nombre comunica que debe respetar las reglas de Hooks.
type UseDebouncedValueOptions = {
delay: number;
};
function useDebouncedValue<T>(value: T, { delay }: UseDebouncedValueOptions) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = window.setTimeout(() => setDebounced(value), delay);
return () => window.clearTimeout(id);
}, [value, delay]);
return debounced;
}Los parámetros deben representar entradas reactivas y el retorno debe ser pequeño y estable conceptualmente.
No extraigas una función solo para ocultar dos líneas. Hazlo cuando exista una abstracción reconocible, reglas propias o reutilización real.
Un useFetch educativo ayuda a aprender effects, pero una aplicación real suele necesitar caché, deduplicación, revalidación, invalidación y SSR. Una biblioteca de server state o el framework resuelve mejor ese dominio.
Para stores externos usa useSyncExternalStore en lugar de reproducir manualmente suscripciones propensas a inconsistencias.
Un custom hook debe exponer las entradas necesarias; no oculte valores reactivos dentro de closures ni desactive el linter.
Prueba el comportamiento observable mediante un componente consumidor o utilidades de renderizado de hooks. No acoples las pruebas al orden interno de llamadas.
Un buen custom hook tiene un propósito reconocible, un contrato pequeño y una implementación que respeta las mismas reglas que cualquier componente.
Un custom hook reutiliza una secuencia de lógica reactiva:
inputs
↓
Hooks internos
↓
valor y accionesNo comparte automáticamente state entre consumidores:
function ComponentA() {
const status = useOnlineStatus();
}
function ComponentB() {
const status = useOnlineStatus();
}Cada llamada tiene su propia secuencia de Hooks. Si ambas leen la misma store externa, comparten la fuente porque el store es compartido, no porque el Hook fusione sus estados.
Antes:
function ChatRoom({ roomId }: Props) {
useEffect(() => {
const connection = connect(roomId);
connection.open();
return () => connection.close();
}, [roomId]);
}Después:
function useChatConnection(roomId: string) {
useEffect(() => {
const connection = connect(roomId);
connection.open();
return () => connection.close();
}, [roomId]);
}El nombre comunica el sistema sincronizado y permite reutilizar la regla completa.
Las entradas deberían expresar el contrato reactivo:
type UseChatOptions = {
roomId: string;
enabled?: boolean;
};Evita un objeto genérico con decenas de flags. Si una opción cambia el lifecycle de forma importante, documenta si reconecta, reinicia state o solo modifica un callback.
Retorno simple:
const isOnline = useOnlineStatus();Retorno compuesto:
const { data, error, retry } = useResource(...);Una tupla funciona bien cuando las posiciones son conocidas, como [state, setState]. Un objeto es más legible cuando hay varias capacidades opcionales.
No expongas cada setter interno. Devuelve acciones de dominio:
return { draft, updateTitle, resetDraft, submitDraft };Un custom hook puede combinar Context y cálculos sin effect:
function useCurrencyFormatter() {
const locale = useContext(LocaleContext);
return useMemo(
() => new Intl.NumberFormat(locale, { style: "currency", currency: "COP" }),
[locale],
);
}No añadas use a una función pura que podría recibir locale como argumento. Usa Hook solo cuando necesita capacidades de React.
useAsync, useBoolean o useFetch pueden ocultar diferencias esenciales:
Una abstracción de dominio como useOrderSearch puede comunicar mejor sus reglas.
Un Hook que sincroniza debe respetar setup, cleanup y dependencias igual que un componente. No esconde problemas:
function useWindowEvent<K extends keyof WindowEventMap>(
type: K,
listener: (event: WindowEventMap[K]) => void,
) {
const onEvent = useEffectEvent(listener);
useEffect(() => {
const handler = (event: WindowEventMap[K]) => onEvent(event);
window.addEventListener(type, handler);
return () => window.removeEventListener(type, handler);
}, [type]);
}El contrato expresa que cambiar type modifica la suscripción, mientras el listener puede leer valores actuales.
function useCurrentUser() {
const session = useSession();
const permissions = usePermissions(session.userId);
return { user: session.user, permissions };
}Componer es útil, pero revisa waterfalls y duplicación de requests. Si ambos Hooks hacen fetching en effects, uno puede comenzar después del otro. Una capa de datos o framework puede coordinar mejor.
Prueba desde comportamiento observable:
function TestConsumer() {
const online = useOnlineStatus();
return <span>{online ? "online" : "offline"}</span>;
}Dispara eventos o cambia la fuente y verifica la salida. Para lógica pura interna, extrae funciones normales y pruébalas directamente.
Un custom hook usado por muchos componentes se convierte en una API. Cambiar forma del retorno, semántica de errores o lifecycle puede romper consumidores. Documenta:
Hooks con browser APIs deben definir un snapshot de servidor o ejecutarse solo en cliente. useSyncExternalStore permite getServerSnapshot. Evita devolver HTML inicial distinto sin una estrategia de hidratación.
useFetch puede ser una abstracción insuficiente?Formularios controlados y no controlados aplica ownership, state y composición a inputs reales.