Busca en todas las páginas de la documentación
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
// Máquina de estado simple con useReducer
type FetchState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: unknown }
| { status: "error"; error: Error };
type FetchEvent =
| { type: "FETCH" }
| { type: "RESOLVE"; data: unknown }
| { type: "REJECT"; error: Error }
| { type: "RESET" };
function fetchReducer(state: FetchState, event: FetchEvent): FetchState {
switch (state.status) {
case "idle":
if (event.type === "FETCH") return { status: "loading" };
return state;
case "loading":
if (event.type === "RESOLVE") return { status: "success", data: event.data };
if (event.type === "REJECT") return { status: "error", error: event.error };
return state;
case "error":
if (event.type === "FETCH") return { status: "loading" };
if (event.type === "RESET") return { status: "idle" };
return state;
case "success":
if (event.type === "FETCH") return { status: "loading" };
if (event.type === "RESET") return { status: "idle" };
return state;
}
}
const [state, send] = useReducer(fetchReducer, { status: "idle" });Cuándo usarlo: Cuando un componente tiene múltiples estados con transiciones restringidas (por ejemplo, un formulario de múltiples pasos, un flujo asincrónico o un modal con carga/error/éxito). Si te encuentras haciendo malabarismos con múltiples booleanos como isLoading, isError, isSuccess, usa una máquina de estado en su lugar.
import { useReducer, useCallback, type ReactNode } from "react";
// --- Máquina de estado de formulario de múltiples pasos ---
interface FormData {
name: string;
email: string;
plan: string;
}
type FormState =
| { step: "details"; data: Partial<FormData> }
| { step: "plan"; data: Partial<FormData> }
| { step: "review"; data: FormData }
| { step: "submitting"; data: FormData }
| { step: "complete"; data: FormData }
| { step: "error"; data: FormData; error: string };
type FormEvent =
| { type: "NEXT"; fields: Partial<FormData> }
| { type: "BACK" }
| { type: "SUBMIT" }
| { type: "SUCCESS" }
| { type: "FAIL"; error: string }
| { type: "RETRY" };
function formReducer(state: FormState, event: FormEvent): FormState {
switch (state.step) {
case "details":
if (event.type === "NEXT") {
return { step: "plan", data: { ...state.data, ...event.fields } };
}
return state;
case "plan":
if (event.type === "NEXT") {
const data = { ...state.data, ...event.fields } as FormData;
return { step: "review", data };
}
if (event.type === "BACK") return { step: "details", data: state.data };
return state;
case "review":
if (event.type === "SUBMIT") return { step: "submitting", data: state.data };
if (event.type === "BACK") return { step: "plan", data: state.data };
return state;
case "submitting":
if (event.type === "SUCCESS") return { step: "complete", data: state.data };
if (event.type === "FAIL") {
return { step: "error", data: state.data, error: event.error };
}
return state;
case "error":
if (event.type === "RETRY") return { step: "submitting", data: state.data };
if (event.type === "BACK") return { step: "review", data: state.data };
return state;
case "complete":
return state; // Estado terminal
}
}
function SignupWizard() {
const [state, send] = useReducer(formReducer, {
step: "details",
data: {},
});
const handleSubmit = useCallback(async () => {
send({ type: "SUBMIT" });
try {
await fetch("/api/signup", {
method: "POST",
body: JSON.stringify(state.step === "review" ? state.data : null),
});
send({ type: "SUCCESS" });
} catch (err) {
send({ type: "FAIL", error: (err as Error).message });
}
}, [state]);
switch (state.step) {
case "details":
return (
<DetailsStep
data={state.data}
onNext={(fields) => send({ type: "NEXT", fields })}
/>
);
case "plan":
return (
<PlanStep
data={state.data}
onNext={(fields) => send({ type: "NEXT", fields })}
onBack={() => send({ type: "BACK" })}
/>
);
case "review":
return (
<ReviewStep
data={state.data}
onSubmit={handleSubmit}
onBack={() => send({ type: "BACK" })}
/>
);
case "submitting":
return <LoadingSpinner message="Creando tu cuenta..." />;
case "error":
return (
<ErrorDisplay
error={state.error}
onRetry={() => send({ type: "RETRY" })}
onBack={() => send({ type: "BACK" })}
/>
);
case "complete":
return <SuccessMessage data={state.data} />;
}
}Lo que esto demuestra:
isLoading && isErroruseReducer es la primitiva React integrada para máquinas de estado. La función reductora ES la máquina de estado.switch en state.step (o state.status) actúa como el gráfico de estado; las verificaciones anidadas de if en event.type definen transiciones válidas.| Concepto | Implementación | Propósito |
|---|---|---|
| Estado | Tipo de unión discriminado | Representa todos los estados posibles con datos asociados |
| Evento | Unión de { type: string; ... } | Todas las entradas posibles que desencadenan transiciones |
| Reductor | (state, event) => state | Función pura que define la lógica de la máquina de estado |
| Despacho | send(event) | Desencadenar una transición de estado |
Condiciones de guardia - permitir transiciones solo cuando se cumplen las condiciones:
case "details":
if (event.type === "NEXT") {
if (!event.fields.name || !event.fields.email) {
return { ...state, validationError: "Todos los campos son requeridos" };
}
return { step: "plan", data: { ...state.data, ...event.fields } };
}
return state;Usar XState para máquinas complejas - cuando la lógica de estado excede lo que useReducer maneja limpiamente:
import { useMachine } from "@xstate/react";
import { createMachine, assign } from "xstate";
const toggleMachine = createMachine({
id: "toggle",
initial: "inactive",
context: { count: 0 },
states: {
inactive: {
on: {
TOGGLE: {
target: "active",
actions: assign({ count: ({ context }) => context.count + 1 }),
},
},
},
active: {
on: { TOGGLE: "inactive" },
},
},
});
function Toggle() {
const [state, send] = useMachine(toggleMachine);
return (
<button onClick={() => send({ type: "TOGGLE" })}>
{state.value} (toggled {state.context.count} times)
</button>
);
}step, status) habilita el estrechamiento de tipos.switch exhaustivo. TypeScript te advertirá si pierdes un estado cuando el tipo de retorno está especificado.boolean en objetos de estado - crean 2^n estados posibles. Usa estados nombrados explícitos en su lugar.typegen para tipos de evento inferidos.Sopa booleana - Usar isLoading, isError, hasData como booleanos separados crea combinaciones imposibles como isLoading && isError. Solución: Reemplaza con un único estado de unión discriminado.
Efectos secundarios en el reductor - Los reductores deben ser funciones puras. Las llamadas a API o mutaciones de DOM dentro del reductor rompen las reglas de React. Solución: Desencadena efectos secundarios fuera del reductor basándose en transiciones de estado (en manejadores de eventos o efectos).
Olvidar el retorno por defecto - Si el reductor no maneja un evento en un estado dado y no devuelve state, el estado se vuelve undefined. Solución: Siempre agrega return state como predeterminado para eventos no manejados en cada caso de estado.
Sobre-ingeniería de estado simple - Un toggle que está activado o desactivado no necesita una máquina de estado. Solución: Usa useState(false) para escenarios triviales de dos estados. Recurre a máquinas cuando tienes 3 o más estados o transiciones complejas.
| Enfoque | Compensación |
|---|---|
Máquina de estado useReducer | Integrada, sin dependencias; lógica de transición manual |
| XState | Poderosa, herramientas visuales; dependencia extra, curva de aprendizaje |
Múltiples booleanos useState | Simple para 1-2 estados; los estados imposibles se vuelven posibles |
| Zustand con campo de estado | Bueno para máquinas de estado global; no es específico de React |
useActionState (React 19) | Diseñado para flujos de envío de formularios; limitado a acciones de formularios |
useReducer es la primitiva React integrada para implementar máquinas de estado.type FetchState =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: unknown }
| { status: "error"; error: Error };data solo existe en success).status), previniendo el acceso a campos inválidos.isLoading && isError se vuelven irrepresentables.isLoading, isError, isSuccess crea combinaciones imposibles.useState(false) es suficiente.switch externo en state.step (o state.status) define en qué estado estás.if en event.type definen qué transiciones son válidas en ese estado.return state predeterminado ignora eventos que no son válidos para el estado actual.useEffect basándose en transiciones de estado.undefined, que puede bloquear el componente o causar errores silenciosos.return state como el caso predeterminado para cada estado en la declaración switch.case "details":
if (event.type === "NEXT") {
if (!event.fields.name || !event.fields.email) {
return { ...state, validationError: "Todos los campos son requeridos" };
}
return { step: "plan", data: { ...state.data, ...event.fields } };
}
return state;never en el caso predeterminado para detectar estados no manejados en tiempo de compilación.useReducer es suficiente y tiene cero dependencias.n campos booleanos crean 2^n combinaciones posibles, la mayoría de las cuales son inválidas.{ isLoading: true, isError: true } es un estado imposible que los booleanos permiten."idle" | "loading" | "error" | "success".useEffect que observe el estado.SUCCESS o FAIL basándose en la respuesta de la API.Revisado por Chris St. John·Última actualización: 16 jul 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥