Volver a React Intermedio
useReducer y Estado Complejo
useReducer es ideal cuando el estado tiene múltiples sub-valores o cuando el siguiente estado depende del anterior de formas complejas.
useReducer Básico
import { useReducer } from 'react';
type Accion =
| { type: 'INCREMENTAR' }
| { type: 'DECREMENTAR' }
| { type: 'REINICIAR' }
| { type: 'ESTABLECER'; payload: number };
function reductor(estado: number, accion: Accion): number {
switch (accion.type) {
case 'INCREMENTAR': return estado + 1;
case 'DECREMENTAR': return estado - 1;
case 'REINICIAR': return 0;
case 'ESTABLECER': return accion.payload;
default: return estado;
}
}
function Contador() {
const [cuenta, despachar] = useReducer(reductor, 0);
return (
<div>
<p>Cuenta: {cuenta}</p>
<button onClick={() => despachar({ type: 'INCREMENTAR' })}>+</button>
<button onClick={() => despachar({ type: 'DECREMENTAR' })}>-</button>
<button onClick={() => despachar({ type: 'REINICIAR' })}>Reiniciar</button>
</div>
);
}
Estado Complejo con useReducer
interface ItemCarrito { id: string; nombre: string; precio: number; cantidad: number; }
interface EstadoCarrito { items: ItemCarrito[]; descuento: number; }
type AccionCarrito =
| { type: 'AGREGAR_ITEM'; item: Omit<ItemCarrito, 'cantidad'> }
| { type: 'ELIMINAR_ITEM'; id: string }
| { type: 'ACTUALIZAR_CANT'; id: string; cantidad: number }
| { type: 'APLICAR_DESCUENTO'; porcentaje: number }
| { type: 'LIMPIAR' };
function reductorCarrito(estado: EstadoCarrito, accion: AccionCarrito): EstadoCarrito {
switch (accion.type) {
case 'AGREGAR_ITEM': {
const existente = estado.items.find(i => i.id === accion.item.id);
if (existente) {
return {
...estado,
items: estado.items.map(i =>
i.id === accion.item.id ? { ...i, cantidad: i.cantidad + 1 } : i
),
};
}
return { ...estado, items: [...estado.items, { ...accion.item, cantidad: 1 }] };
}
case 'ELIMINAR_ITEM':
return { ...estado, items: estado.items.filter(i => i.id !== accion.id) };
case 'ACTUALIZAR_CANT':
return {
...estado,
items: estado.items.map(i =>
i.id === accion.id ? { ...i, cantidad: Math.max(0, accion.cantidad) } : i
),
};
case 'APLICAR_DESCUENTO':
return { ...estado, descuento: accion.porcentaje };
case 'LIMPIAR':
return { items: [], descuento: 0 };
default:
return estado;
}
}
Combinar useReducer con Context
const ContextoCarrito = createContext<{
estado: EstadoCarrito;
despachar: React.Dispatch<AccionCarrito>;
} | null>(null);
function ProveedorCarrito({ children }: { children: React.ReactNode }) {
const [estado, despachar] = useReducer(reductorCarrito, { items: [], descuento: 0 });
return (
<ContextoCarrito.Provider value={{ estado, despachar }}>
{children}
</ContextoCarrito.Provider>
);
}
function useCarrito() {
const ctx = useContext(ContextoCarrito);
if (!ctx) throw new Error('useCarrito debe usarse dentro de ProveedorCarrito');
return ctx;
}
// Estado derivado como selector
function useTotalCarrito() {
const { estado } = useCarrito();
return useMemo(() => {
const subtotal = estado.items.reduce((sum, item) => sum + item.precio * item.cantidad, 0);
return subtotal * (1 - estado.descuento / 100);
}, [estado.items, estado.descuento]);
}