Escribiendo ganchos de React: useState, useEffect, useReducer y ganchos personalizados
Aprende cómo escribir correctamente useState, useEffect, useReducer y ganchos personalizados en TypeScript, además de cuándo elegir TypeScript en lugar de JavaScript puro realmente compensa.
Los errores de tipo detectados en tiempo de compilación evitan sesiones de depuración que de lo contrario podrían extenderse hasta altas horas de la noche, y esta es precisamente la razón por la cual los equipos que combinan React con TypeScript consideran a los hooks tipados como algo esencial y no como un lujo. Familiarizarse con cómo tipar el estado, los efectos, los reducers y los hooks personalizados, así como saber cuándo recurrir a TypeScript es la decisión adecuada desde un principio, son dos aspectos de la misma habilidad práctica necesaria para desarrollar aplicaciones React fiables.
Por qué la seguridad de tipos debe formar parte de tus hooks
Los React Hooks ya ofrecen a los desarrolladores una forma más limpia y modular de estructurar componentes. TypeScript añade una capa de seguridad sobre esa estructura, de modo que los errores se detecten mientras se escribe el código y no después de que un usuario se encuentre con una pantalla defectuosa en producción.
Los hooks fueron diseñados desde el principio para que la lógica reutilizable fuera predecible, un punto que Dan Abramov ha mencionado al hablar sobre la filosofía de diseño de React. La contribución de TypeScript es tomar esa predecibilidad y hacerla explícita y verificable, en lugar de algo que tengas que tener en mente.
Escribir useState correctamente
En la mayoría de los casos cotidianos, TypeScript determina por sí mismo el tipo del estado sin necesidad de ayuda alguna:
// inferred as boolean, no annotation needed
const [isLoading, setIsLoading] = useState(false);
Las cosas cambian cuando el valor inicial es null o undefined; en ese caso, debes anotar el tipo tú mismo en lugar de confiar en la inferencia:
interface UserProfile {
id: string;
name: string;
avatarUrl: string;
}
const [user, setUser] = useState<UserProfile | null>(null);
Un hábito útil aquí es resistirse a la tentación de recurrir a un tipo genérico solo para que desaparezca el mensaje de error. Hacerlo elimina silenciosamente todo el beneficio que intentabas obtener de TypeScript desde un principio.
Escribir useEffect: Menos complicado de lo esperado
Los genericos en realidad no se aplican a useEffect, pero aún así es necesario tener cuidado con las arrays de dependencias y la lógica de limpieza:
useEffect(() => {
const controller = new AbortController();
const fetchUser = async () => {
const res = await fetch(`/api/users/${userId}`, {
signal: controller.signal,
});
const data: UserProfile = await res.json();
setUser(data);
};
fetchUser();
return () => controller.abort(); // cleanup on unmount
}, [userId]);
Escribir useReducer para estados más complejos
Este es el hook donde la ventaja de TypeScript se vuelve evidente:
type CartAction =
| { type: 'ADD_ITEM'; payload: CartItem }
| { type: 'REMOVE_ITEM'; payload: string }
| { type: 'CLEAR_CART' };
function cartReducer(state: CartItem[], action: CartAction): CartItem[] {
switch (action.type) {
case 'ADD_ITEM':
return [...state, action.payload];
case 'REMOVE_ITEM':
return state.filter((item) => item.id !== action.payload);
case 'CLEAR_CART':
return [];
default:
return state;
}
}
Al modelar los tipos de acción de esta manera, enviar una acción inválida se convierte en un error en tiempo de compilación que se detecta de inmediato, en lugar de ser una sorpresa en tiempo de ejecución que se descubre más tarde.
Escribir hooks personalizados reutilizables con genericos
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
const stored = window.localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
window.localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
Dado que este hook es genérico, puede reutilizarse en cualquier parte de tu códigobase —con cadenas de texto, objetos o arrays— manteniendo una precisión total en los tipos para cualquier dato que se le pase.
La versión breve
- Deje que TypeScript infiera el estado simple por sí mismo, pero sea explícito siempre que un valor pueda ser null o undefined.
- Modele las acciones de useReducer como una unión discriminada.
- Use generics en los hooks personalizados para que sean reutilizables con diferentes tipos de datos.
- Trate
anycomo un atajo que, en silencio, le cuesta más de lo que ahorra posteriormente.
Elegir entre JavaScript puro y TypeScript
Una vez que comprenda cuánta seguridad le brindan los hooks tipados, surge naturalmente una pregunta más amplia: ¿debería cada proyecto usar realmente TypeScript, o es eso a veces sobreingeniería? Durante mucho tiempo, esto se planteó como una elección binaria, y elegir un lado significaba lidiar con verdaderos compromisos.
En el pasado, optar por TypeScript significaba lidiar con pipelines de compilación, herramientas como ts-node y una montaña de archivos de configuración solo para hacer que un script funcionara. Ahora que entornos como Node.js admiten el eliminación nativa de tipos, esa fricción ha desaparecido en gran medida, y la diferencia entre escribir JavaScript puro y TypeScript es mucho menor que antes.
Al analizar cómo cada opción afecta tu flujo de trabajo diario, resulta más fácil decidir cuál se adapta realmente al proyecto que tienes entre manos.
Por qué el JavaScript puro sigue teniendo su lugar
JavaScript es el lenguaje que el navegador y Node.js entienden de forma nativa. Escribes un archivo, lo ejecutas y se ejecuta de inmediato, sin que ningún compilador se interponga ni sea necesario declarar nada adicional.
- Prototipado instantáneo: cuando estás probando una idea, creando un script rápido o desarrollando un MVP pequeño, JavaScript te permite actuar tan rápido como puedas pensar.
- Sin necesidad de configuración: no es necesario contar con un
tsconfig.jsonni realizar pasos de verificación de tipos solo para confirmar que una función hace lo esperado. - Menos carga mental: tu atención se mantiene en la lógica real del programa en lugar de en declaraciones de tipo o errores del compilador.
La desventaja aparece cuando una base de código en JavaScript supera unas pocas mil líneas. En ese punto, el refactoring se vuelve arriesgado: renombrar una propiedad en veinte archivos implica depender de una función global de buscar y reemplazar, con la esperanza de que nada se rompa silenciosamente al ejecutar la aplicación.
Por qué TypeScript resulta beneficioso a medida que crecen los proyectos
TypeScript superpone un sistema de tipos estáticos sobre JavaScript, funcionando como un revisor automático que señala los problemas a medida que escribes; por ejemplo, te advierte en el momento en que intentas pasar una cadena de texto a una función que espera un número.
- Documentación siempre precisa: los tipos sirven también como documentación dinámica. Una interfaz te indica la forma exacta que debe tener un objeto sin obligarte a revisar varios archivos de implementación.
- Refactorizaciones más seguras: si modificas un modelo de datos o un esquema de base de datos, el compilador indica cada ubicación en la base de código que ahora necesita actualización.
- Mejor soporte del editor: herramientas como VS Code o Cursor se basan en el servidor de lenguaje de TypeScript para ofrecer autocompletado rápido y resaltado de errores en línea mientras trabajas.
Históricamente, el compromiso implicaba un verdadero “impuesto por herramientas”: la configuración de compiladores, mapas de código fuente y scripts de envoltura añadía pasos adicionales a lo que antes era un flujo de trabajo sencillo.
Por qué ese compromiso ya no aplica de la misma manera
La antigua queja de que “la configuración de TypeScript es demasiado engorrosa” ya no es tan válida. Las versiones actuales de Node.js pueden ejecutar archivos TypeScript directamente, sin una etapa de compilación separada, gracias a la eliminación de tipos.
Detrás de escena, el entorno en tiempo de ejecución simplemente elimina las anotaciones de tipo y las declaraciones de interfaces, dejando JavaScript puro que se puede ejecutar de inmediato. Eso significa que mantienes la seguridad de los tipos estáticos durante el desarrollo sin la sobrecarga de configuración que antes venía con ellos.
Elegir la herramienta adecuada para el trabajo
Para un script desechable, una tarea de automatización sencilla o un proyecto destinado únicamente a aprender cómo funciona la web, JavaScript puro sigue siendo la mejor opción. Evitar la estructura adicional mantiene el proceso ligero y agradable.
Para casi todo lo demás: repositorios de código en equipo, aplicaciones con varios modelos de datos interactivos o cualquier proyecto que se pretenda mantener más allá de un mes, la pequeña cantidad de configuración que requiere TypeScript vale la pena. Dado que los entornos de ejecución modernos manejan la ejecución sin problemas en ambos casos, en realidad no es necesario elegir entre flexibilidad y seguridad: tanto la simplicidad de JavaScript como las medidas de protección de TypeScript están disponibles, y elegir uno se trata principalmente de adaptarlo al tiempo que durará el proyecto y al grado de colaboración necesario.
Lecturas relacionadas
- Los valores por defecto incompatibles de TypeScript 6.0: Una guía práctica de migración — Aprenda cuáles son los nueve valores por defecto del compilador de TypeScript 6.0 que han cambiado, cómo configurar tsconfig para 2026 y cómo preparar los repositorios de código para TypeScript 7 basado en Go.
- Los valores por defecto de full-stack JavaScript en 2026: TypeScript, RSC y más — Explica por qué TypeScript, React Server Components y un enfoque de gestión de estado más eficiente se han convertido en la stack estándar para equipos de JavaScript en 2026.