Capítulo 20: Frontend React para Tracking de Pedidos
Capítulo 20: Frontend React para Tracking de Pedidos
“Visibilidad en tiempo real del progreso de la saga”
Introduccion
Una saga puede tardar segundos o minutos en completarse. Durante ese tiempo, el usuario necesita feedback sobre el progreso de su pedido. Este capitulo implementa una interfaz React que muestra el estado de la saga en tiempo real.
Tecnologias que usaremos:
- React: Biblioteca de UI para construir interfaces declarativas
- WebSocket: Protocolo para comunicacion bidireccional en tiempo real
- Zustand: Biblioteca ligera de gestion de estado para React
El resultado final sera una pagina de tracking que muestra cada paso de la saga actualizandose automaticamente mientras el backend procesa.
Arquitectura del Frontend
El diagrama muestra como el frontend se conecta al backend:
graph LR
R[React App] --> WS[WebSocket]
WS --> API[API Gateway]
API --> S[Saga Orchestrator]
R --> REST[REST API]
REST --> API
Hook para WebSocket
Un hook en React es una funcion que encapsula logica reutilizable. useSagaTracking maneja la conexion WebSocket:
- useEffect: Se ejecuta cuando
sagaIdcambia, creando una nueva conexion - WebSocket: Establece conexion bidireccional con el servidor
- Eventos:
onopen,onmessage,onerror,onclosemanejan el ciclo de vida - Cleanup: La funcion de retorno cierra la conexion al desmontar el componente
El hook retorna tres valores: el estado actual de la saga, si estamos conectados, y cualquier error.
// hooks/useSagaTracking.ts
import { useEffect, useState, useCallback } from 'react';
interface SagaStep {
name: string;
status: 'pending' | 'running' | 'completed' | 'failed' | 'compensating';
startedAt?: Date;
completedAt?: Date;
error?: string;
}
interface SagaState {
sagaId: string;
orderId?: string;
status: 'running' | 'completed' | 'failed' | 'compensating';
steps: SagaStep[];
createdAt: Date;
}
export function useSagaTracking(sagaId: string | null) {
const [state, setState] = useState<SagaState | null>(null);
const [connected, setConnected] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!sagaId) return;
const ws = new WebSocket(`ws://localhost:3001/saga/${sagaId}`);
ws.onopen = () => {
setConnected(true);
setError(null);
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
setState(data);
};
ws.onerror = () => {
setError('Connection error');
};
ws.onclose = () => {
setConnected(false);
};
return () => ws.close();
}, [sagaId]);
return { state, connected, error };
}
Componente de Tracking
El componente OrderTracking visualiza el progreso de la saga:
- Renderizado condicional: Muestra error, loading o el contenido segun el estado
- StatusBadge: Componente auxiliar que muestra el estado con colores
- StepItem: Muestra cada paso con iconos que indican su estado
Los iconos representan estados:
○Pendiente◐En progreso●Completado✕Fallido↺Compensando
// components/OrderTracking.tsx
import { useSagaTracking } from '../hooks/useSagaTracking';
interface Props {
sagaId: string;
}
const stepLabels: Record<string, string> = {
CREATE_ORDER: 'Crear Pedido',
RESERVE_STOCK: 'Reservar Stock',
PROCESS_PAYMENT: 'Procesar Pago',
SCHEDULE_SHIPPING: 'Programar Envío',
NOTIFY_CUSTOMER: 'Notificar Cliente'
};
export function OrderTracking({ sagaId }: Props) {
const { state, connected, error } = useSagaTracking(sagaId);
if (error) {
return <div className="text-red-500">Error: {error}</div>;
}
if (!state) {
return <div className="animate-pulse">Cargando...</div>;
}
return (
<div className="max-w-md mx-auto p-4">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold">Pedido #{state.orderId}</h2>
<StatusBadge status={state.status} />
</div>
<div className="space-y-4">
{state.steps.map((step, index) => (
<StepItem key={step.name} step={step} index={index} />
))}
</div>
{!connected && (
<div className="mt-4 text-sm text-yellow-600">
Reconectando...
</div>
)}
</div>
);
}
function StatusBadge({ status }: { status: string }) {
const colors: Record<string, string> = {
running: 'bg-blue-100 text-blue-800',
completed: 'bg-green-100 text-green-800',
failed: 'bg-red-100 text-red-800',
compensating: 'bg-yellow-100 text-yellow-800'
};
return (
<span className={`px-2 py-1 rounded text-sm ${colors[status]}`}>
{status}
</span>
);
}
function StepItem({ step, index }: { step: any; index: number }) {
const icons: Record<string, string> = {
pending: '○',
running: '◐',
completed: '●',
failed: '✕',
compensating: '↺'
};
return (
<div className="flex items-center gap-3">
<span className="text-xl">{icons[step.status]}</span>
<div className="flex-1">
<div className="font-medium">{stepLabels[step.name] || step.name}</div>
{step.error && (
<div className="text-sm text-red-500">{step.error}</div>
)}
</div>
{step.status === 'running' && (
<div className="w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" />
)}
</div>
);
}
Store con Zustand
Zustand es una biblioteca de gestion de estado minimalista para React. A diferencia de Redux, no requiere actions, reducers ni providers.
El store useOrderStore maneja:
- items: Productos en el carrito
- sagaId: ID de la saga actual (null si no hay pedido en proceso)
- status: Estado del flujo (idle, submitting, tracking)
Las funciones (addItem, submitOrder, etc.) modifican el estado. set() actualiza el store y React re-renderiza automaticamente los componentes que lo usan.
// stores/orderStore.ts
import { create } from 'zustand';
interface OrderItem {
productId: string;
name: string;
quantity: number;
price: number;
}
interface OrderState {
items: OrderItem[];
sagaId: string | null;
status: string;
addItem: (item: OrderItem) => void;
removeItem: (productId: string) => void;
submitOrder: () => Promise<void>;
reset: () => void;
}
export const useOrderStore = create<OrderState>((set, get) => ({
items: [],
sagaId: null,
status: 'idle',
addItem: (item) => {
set((state) => ({
items: [...state.items, item]
}));
},
removeItem: (productId) => {
set((state) => ({
items: state.items.filter((i) => i.productId !== productId)
}));
},
submitOrder: async () => {
set({ status: 'submitting' });
const response = await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
items: get().items,
total: get().items.reduce((sum, i) => sum + i.price * i.quantity, 0)
})
});
const data = await response.json();
set({ sagaId: data.sagaId, status: 'tracking' });
},
reset: () => {
set({ items: [], sagaId: null, status: 'idle' });
}
}));
Pagina de Checkout
La pagina de checkout orquesta todo el flujo del usuario:
- Estado idle: Muestra el carrito y boton de confirmar
- Estado submitting: Muestra “Procesando…” mientras espera respuesta del API
- Estado tracking: Muestra el componente
OrderTrackingcon progreso en tiempo real
El flujo completo:
- Usuario revisa carrito y hace clic en “Confirmar Pedido”
submitOrder()envia items al API y recibe unsagaId- Se muestra
OrderTrackingque conecta por WebSocket al saga - El usuario ve cada paso actualizarse en tiempo real
// pages/Checkout.tsx
import { useOrderStore } from '../stores/orderStore';
import { OrderTracking } from '../components/OrderTracking';
export function CheckoutPage() {
const { items, sagaId, status, submitOrder, reset } = useOrderStore();
const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0);
if (status === 'tracking' && sagaId) {
return (
<div className="p-6">
<OrderTracking sagaId={sagaId} />
<button
onClick={reset}
className="mt-4 px-4 py-2 bg-gray-200 rounded"
>
Nueva orden
</button>
</div>
);
}
return (
<div className="max-w-lg mx-auto p-6">
<h1 className="text-2xl font-bold mb-4">Checkout</h1>
<div className="space-y-2 mb-4">
{items.map((item) => (
<div key={item.productId} className="flex justify-between">
<span>{item.name} x{item.quantity}</span>
<span>${item.price * item.quantity}</span>
</div>
))}
</div>
<div className="border-t pt-2 mb-4">
<div className="flex justify-between font-bold">
<span>Total</span>
<span>${total.toFixed(2)}</span>
</div>
</div>
<button
onClick={submitOrder}
disabled={status === 'submitting' || items.length === 0}
className="w-full py-2 bg-blue-500 text-white rounded disabled:opacity-50"
>
{status === 'submitting' ? 'Procesando...' : 'Confirmar Pedido'}
</button>
</div>
);
}
WebSocket Server (Backend)
El servidor WebSocket en el backend maneja las conexiones de clientes:
- WebSocketServer: Servidor que acepta conexiones WebSocket
- clients Map: Rastrea que clientes estan suscritos a cada saga
- notify(): Funcion que envia actualizaciones a todos los clientes interesados en una saga
Cuando el orquestador de saga actualiza el estado, llama a notify(sagaId, newState), y todos los clientes conectados reciben la actualizacion instantaneamente.
// server/websocket.ts
import { WebSocketServer, WebSocket } from 'ws';
import { SagaRepository } from '../saga/repository';
export function setupWebSocket(server: any, repository: SagaRepository) {
const wss = new WebSocketServer({ server, path: '/saga' });
const clients = new Map<string, Set<WebSocket>>();
wss.on('connection', (ws, req) => {
const sagaId = req.url?.split('/').pop();
if (!sagaId) return ws.close();
if (!clients.has(sagaId)) {
clients.set(sagaId, new Set());
}
clients.get(sagaId)!.add(ws);
// Enviar estado inicial
repository.findById(sagaId).then((state) => {
if (state) ws.send(JSON.stringify(state));
});
ws.on('close', () => {
clients.get(sagaId)?.delete(ws);
});
});
// Función para notificar cambios
return {
notify(sagaId: string, state: any) {
const sagaClients = clients.get(sagaId);
if (!sagaClients) return;
const message = JSON.stringify(state);
sagaClients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
};
}
Resumen
- WebSocket para actualizaciones en tiempo real
- Zustand para estado del carrito
- Componentes visuales muestran progreso de pasos
- Reconexion automatica ante perdida de conexion
Glosario
React Hook
Definicion: Funcion especial de React (prefijo use) que permite usar estado y otras caracteristicas de React en componentes funcionales.
Por que es importante: Permite encapsular y reutilizar logica compleja (como conexiones WebSocket) de forma limpia y declarativa.
Ejemplo practico: useSagaTracking(sagaId) encapsula toda la logica de WebSocket. El componente solo necesita llamar al hook y usar los valores que retorna.
WebSocket
Definicion: Protocolo de comunicacion que proporciona canales de comunicacion bidireccional full-duplex sobre una conexion TCP persistente.
Por que es importante: Permite push de datos del servidor al cliente sin que este tenga que hacer polling, ideal para actualizaciones en tiempo real.
Ejemplo practico: Cuando el paso “Procesar Pago” se completa en el backend, el servidor envia un mensaje WebSocket y el UI se actualiza instantaneamente.
Zustand
Definicion: Biblioteca de gestion de estado para React que ofrece una API simple basada en hooks, sin el boilerplate de Redux.
Por que es importante: Simplifica la gestion de estado compartido entre componentes, con excelente rendimiento y tipado TypeScript.
Ejemplo practico: const { items, addItem } = useOrderStore() accede al carrito desde cualquier componente sin pasar props.
Renderizado Condicional
Definicion: Tecnica en React donde diferentes elementos se renderizan segun condiciones, usando operadores ternarios, && o early returns.
Por que es importante: Permite mostrar diferentes UIs segun el estado de la aplicacion (loading, error, exito) de forma declarativa.
Ejemplo practico: if (error) return <Error />; if (!state) return <Loading />; return <Content /> muestra la UI apropiada segun el estado.
useEffect
Definicion: Hook de React que ejecuta efectos secundarios (suscripciones, llamadas API, manipulacion DOM) despues del renderizado.
Por que es importante: Permite sincronizar el componente con sistemas externos como WebSockets, APIs o el DOM.
Ejemplo practico: El efecto crea la conexion WebSocket cuando el componente se monta y la cierra cuando se desmonta, evitando memory leaks.
Cleanup Function
Definicion: Funcion retornada por useEffect que React ejecuta antes de re-ejecutar el efecto o cuando el componente se desmonta.
Por que es importante: Previene memory leaks y comportamientos inesperados al limpiar recursos como conexiones, timers o suscripciones.
Ejemplo practico: return () => ws.close() cierra la conexion WebSocket cuando el usuario navega a otra pagina, liberando recursos.
Estado de Conexion
Definicion: Variable que rastrea si una conexion (WebSocket, API) esta activa, permitiendo mostrar indicadores visuales al usuario.
Por que es importante: Proporciona feedback al usuario sobre problemas de conectividad, mejorando la experiencia en redes inestables.
Ejemplo practico: Si connected === false, mostramos “Reconectando…” para que el usuario sepa que las actualizaciones pueden estar retrasadas.
Push vs Pull
Definicion: Dos modelos de comunicacion: push (el servidor envia datos cuando hay cambios) vs pull (el cliente pregunta periodicamente).
Por que es importante: Push (WebSocket) es mas eficiente y proporciona actualizaciones inmediatas; pull (polling) es mas simple pero genera trafico innecesario.
Ejemplo practico: En lugar de consultar cada 2 segundos “¿cambio el estado?”, el servidor nos avisa instantaneamente cuando hay un cambio.