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]); }