//
Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Tipifica correctamente el state de tu componente con useState y useReducer. Maneja valores simples, objetos complejos, state nullable, y reductores de unión discriminada.
// useState simple - el tipo se infiere
const [count, setCount] = useState(0); // number
const [name, setName] = useState(""); // string
const [enabled, setEnabled] = useState(false); // boolean// Tipificación explícita para state complejo o nullable
type User = {
id: string;
name: string;
email: string;
};
const [user, setUser] = useState<User | null>(null);
// Más tarde...
if (user) {
console.log(user.name); // TypeScript sabe que user no es null aquí
}// useReducer con acciones de unión discriminada
type CounterState = {
count: number;
lastAction: string;
};
type CounterAction =
| { type: "increment"; payload: number }
| { type: "decrement"; payload: number }
| { type: "reset" };
function counterReducer(state: CounterState, action: CounterAction): CounterState {
switch (action.type) {
case "increment":
return { count: state.count + action.payload, lastAction: "increment" };
case "decrement":
return { count: state.count - action.payload, lastAction: "decrement" };
case "reset":
return { count: 0, lastAction: "reset" };
}
}
function Counter() {
const [state, dispatch] = useReducer(counterReducer, { count: 0, lastAction: "none" });
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: "increment", payload: 1 })}>+1</button>
<button onClick={() => dispatch({ type: "reset" })}>Reset</button>
</div>
);
}useState infiere el tipo del valor inicial. useState(0) te da [number, Dispatch<SetStateAction<number>>].null pero eventualmente manteniendo un objeto), debes proporcionar un parámetro de tipo explícito: useState<User | null>(null).useReducer infiere los tipos de state y action de la firma de la función reductora. Definir el reductor con tipos de parámetro explícitos te da seguridad de tipos completa en el cuerpo del reductor y en las llamadas dispatch.type literal) permiten que TypeScript estreche la action dentro de cada rama case, dándote acceso a campos payload específicos de la rama.Inicializador perezoso:
const [data, setData] = useState<Map<string, User>>(() => new Map());State con una tupla:
const [coords, setCoords] = useState<[number, number]>([0, 0]);useReducer con Immer:
import { useImmerReducer } from "use-immer";
function reducer(draft: CounterState, action: CounterAction) {
switch (action.type) {
case "increment":
draft.count += action.payload;
draft.lastAction = "increment";
break;
case "reset":
draft.count = 0;
draft.lastAction = "reset";
break;
}
}
const [state, dispatch] = useImmerReducer(reducer, { count: 0, lastAction: "none" });SetStateAction<T> es T | ((prev: T) => T). Esto es por qué tanto setCount(5) como setCount(prev => prev + 1) funcionan.useReducer, el tipo de retorno del reductor debe coincidir con el tipo del state. TypeScript lo aplica automáticamente cuando anotas los parámetros del reductor.as con state. Si TypeScript se queja, usualmente significa que tus tipos necesitan ajuste, no un cast.useState<User>() sin un valor inicial te da User | undefined, no User. Siempre proporciona un valor inicial o tipifica explícitamente como User | undefined.setUser sin propagar el state anterior reemplaza el objeto completo. TypeScript capturará campos requeridos faltantes, lo cual es en realidad útil.any derrota el propósito. Incluso para formas dinámicas, usa Record<string, unknown> o un tipo adecuado.null en state nullable lleva a errores en tiempo de ejecución que TypeScript intenta prevenir mediante verificaciones estrictas de null.| Enfoque | Pros | Contras |
|---|---|---|
useState con inferencia | Cero boilerplate para valores simples | No puede expresar estados iniciales nullable o union |
useState<T> genérico explícito | Control completo sobre el tipo de state | Ligeramente más verboso |
useReducer | Transiciones de state predecibles, excelente para state complejo | Más boilerplate que useState |
| Zustand store | State compartido con inferencia de TypeScript | Dependencia externa |
useActionState (React 19) | Gestión de state de formulario integrada | Limitado a patrones de formulario/action |
useState(0) infiere number, useState("") infiere string.useState<User | null>(null).useState<[number, number]>([0, 0]).SetStateAction<T> se define como T | ((prev: T) => T).setCount(5) como setCount(prev => prev + 1) son válidos.type literal: { type: "increment"; payload: number } | { type: "reset" }.switch, dando acceso a campos específicos de la rama como payload.const [data, setData] = useState<Map<string, User>>(() => new Map());User | undefined, no User.User | undefined.useState reemplazan el valor completo. A diferencia del setState del componente de clase, no hay fusión.setUser(prev => ({ ...prev, ...newPartialData })).Record<string, unknown> o define un tipo adecuado.case retorna una forma que le falta un campo requerido, obtienes un error en tiempo de compilación.import { useImmerReducer } from "use-immer";
function reducer(draft: CounterState, action: CounterAction) {
switch (action.type) {
case "increment":
draft.count += action.payload;
break;
}
}useImmerReducer infiere tipos de la firma del reductor al igual que useReducer.useReducer está integrado, limitado a un componente, y tiene cero dependencias.useState<User | null>(null).if (user) { user.name }.Revisado por Chris St. John·Última actualización: 7 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥