Skip to content

Patrones de transformación práctica

Los operadores de transformación son uno de los grupos de operadores más frecuentemente usados en RxJS. Desempeñan un papel indispensable para procesar y transformar datos flexiblemente en programación reactiva.

Esta sección organiza patrones de uso de operadores de transformación mientras introduce ejemplos prácticos típicos.

💬 Patrones de uso típicos

PatrónOperadores representativosDescripción
Transformación simple de valoresmapAplicar función de transformación a cada valor
Procesamiento acumulativo/agregaciónscan, reduceAcumular valores secuencialmente
Procesamiento asíncrono anidadomergeMap, switchMap, concatMap, exhaustMapGenerar y combinar Observables
Procesamiento por lotes/agrupaciónbufferTime, bufferCount, windowTimeProcesar agrupadamente, gestión dividida
Extracción de propiedadespluckExtraer campo específico de objeto

Validación y transformación de entrada de usuario

ts
import { fromEvent } from 'rxjs';
import { map, debounceTime, distinctUntilChanged } from 'rxjs';

// Campo de entrada
const emailInput = document.createElement('input');
const emailStatus = document.createElement('p');
document.body.appendChild(emailInput);
document.body.appendChild(emailStatus);

// Función de validación de dirección de correo
function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

// Procesamiento de entrada
fromEvent(emailInput, 'input')
  .pipe(
    debounceTime(400),
    map((event) => (event.target as HTMLInputElement).value.trim()),
    distinctUntilChanged(),
    map((email) => {
      if (!email) {
        return {
          isValid: false,
          message: 'Por favor ingrese dirección de correo',
          value: email,
        };
      }

      if (!isValidEmail(email)) {
        return {
          isValid: false,
          message: 'Por favor ingrese dirección de correo válida',
          value: email,
        };
      }

      return {
        isValid: true,
        message: 'La dirección de correo es válida',
        value: email,
      };
    })
  )
  .subscribe((result) => {
    if (result.isValid) {
      emailStatus.textContent = '✓ ' + result.message;
      emailStatus.className = 'valid';
    } else {
      emailStatus.textContent = '✗ ' + result.message;
      emailStatus.className = 'invalid';
    }
  });

Transformación y agregación de array de objetos

ts
import { from } from 'rxjs';
import { map, toArray } from 'rxjs';

// Datos de ventas
const sales = [
  { product: 'Laptop', price: 120000, quantity: 3 },
  { product: 'Tablet', price: 45000, quantity: 7 },
  { product: 'Smartphone', price: 85000, quantity: 4 },
  { product: 'Mouse', price: 3500, quantity: 12 },
  { product: 'Keyboard', price: 6500, quantity: 8 },
];

// Transformación y agregación de datos
from(sales)
  .pipe(
    // Calcular monto total por producto
    map((item) => ({
      product: item.product,
      price: item.price,
      quantity: item.quantity,
      total: item.price * item.quantity,
    })),
    // Añadir precio con impuestos
    map((item) => ({
      ...item,
      totalWithTax: Math.round(item.total * 1.1),
    })),
    // Convertir de nuevo a array
    toArray(),
    // Calcular monto total
    map((items) => {
      const grandTotal = items.reduce((sum, item) => sum + item.total, 0);
      const grandTotalWithTax = items.reduce(
        (sum, item) => sum + item.totalWithTax,
        0
      );
      return {
        items,
        grandTotal,
        grandTotalWithTax,
      };
    })
  )
  .subscribe((result) => {
    console.log('Detalles de productos:', result.items);
    console.log('Monto total (sin impuestos):', result.grandTotal);
    console.log('Monto total (con impuestos):', result.grandTotalWithTax);
  });
// Salida:
// Detalles de productos: (5) [{…}, {…}, {…}, {…}, {…}]
// Monto total (sin impuestos): 1109000
// Monto total (con impuestos): 1219900

Normalización de datos JSON

ts
import { ajax } from 'rxjs/ajax';
import { map } from 'rxjs';

const resultBox = document.createElement('div');
resultBox.id = 'normalized-results';
document.body.appendChild(resultBox);

ajax
  .getJSON<any[]>('https://jsonplaceholder.typicode.com/users')
  .pipe(
    map((users) => {
      // Convertir a objeto con ID como clave
      const normalizedUsers: Record<number, any> = {};
      const userIds: number[] = [];

      users.forEach((user) => {
        normalizedUsers[user.id] = {
          ...user,
          // Aplanar objeto anidado
          companyName: user.company.name,
          city: user.address.city,
          street: user.address.street,
          // Eliminar anidamiento innecesario
          company: undefined,
          address: undefined,
        };
        userIds.push(user.id);
      });

      return {
        entities: normalizedUsers,
        ids: userIds,
      };
    })
  )
  .subscribe((result) => {
    const title = document.createElement('h3');
    title.textContent = 'Datos de usuario normalizados';
    resultBox.appendChild(title);

    result.ids.forEach((id) => {
      const user = result.entities[id];
      const div = document.createElement('div');
      div.innerHTML = `
      <strong>${user.name}</strong><br>
      Nombre de usuario: @${user.username}<br>
      Email: ${user.email}<br>
      Empresa: ${user.companyName}<br>
      Dirección: ${user.city}, ${user.street}<br><br>
    `;
      resultBox.appendChild(div);
    });

    // Acceso rápido a usuario con ID específico posible
    console.log('Usuario ID 3:', result.entities[3]);
  });

Combinación de múltiples transformaciones

En aplicaciones reales, es común usar múltiples operadores de transformación en combinación.

ts
import { fromEvent, timer } from 'rxjs';
import {
  switchMap,
  map,
  tap,
  debounceTime,
  takeUntil,
  distinctUntilChanged,
} from 'rxjs';
import { ajax } from 'rxjs/ajax';

type User = {
  id: number;
  name: string;
  username: string;
  email: string;
  company: {
    name: string;
  };
};

// Entrada de búsqueda
const searchInput = document.createElement('input');
const resultsContainer = document.createElement('p');
const loadingIndicator = document.createElement('p');

document.body.append(searchInput);
document.body.append(resultsContainer);
document.body.append(loadingIndicator);

// Procesamiento de búsqueda
fromEvent(searchInput, 'input')
  .pipe(
    // Obtener valor de entrada
    map((event) => (event.target as HTMLInputElement).value.trim()),
    // Esperar 300ms
    debounceTime(300),
    // Ignorar si es el mismo valor
    distinctUntilChanged(),
    // Mostrar indicador de carga
    tap(() => {
      loadingIndicator.style.display = 'block';
      resultsContainer.innerHTML = '';
    }),
    // Solicitud API (cancelar solicitud anterior)
    switchMap((term) => {
      // Sin resultados si entrada vacía
      if (term === '') {
        return [];
      }

      // Procesamiento de timeout (5 segundos)
      const timeout$ = timer(5000).pipe(
        tap(() => console.warn('Timeout de respuesta API')),
        map(() => [{ error: 'Timeout' }])
      );

      // Llamada API
      const response$ = ajax
        .getJSON(
          `https://jsonplaceholder.typicode.com/users?username_like=${term}`
        )
        .pipe(
          // Procesar resultados
          map((users) =>
            (users as User[]).map((user) => ({
              id: user.id,
              name: user.name,
              username: user.username,
              email: user.email,
              company: user.company.name,
            }))
          ),
          // Completar antes del timeout
          takeUntil(timeout$)
        );

      return response$;
    }),
    // Finalizar carga
    tap(() => {
      loadingIndicator.style.display = 'none';
    })
  )
  .subscribe((result) => {
    loadingIndicator.style.display = 'none';

    if (Array.isArray(result)) {
      if (result.length === 0) {
        resultsContainer.innerHTML =
          '<div class="no-results">Usuario no encontrado</div>';
      } else {
        resultsContainer.innerHTML = result
          .map(
            (user) => `
          <div class="user-card">
            <h3>${user.name}</h3>
            <p>@${user.username}</p>
            <p>${user.email}</p>
            <p>Empresa: ${user.company}</p>
          </div>
        `
          )
          .join('');
      }
    } else {
      resultsContainer.innerHTML = `<div class="error">⚠️ ${result}</div>`;
    }
  });

🧠 Resumen

  • Transformación simple: map
  • Si se maneja procesamiento asíncrono: mergeMap, switchMap, concatMap, exhaustMap
  • Procesamiento por lotes: bufferTime, bufferCount
  • Extracción de propiedades: pluck
  • En aplicaciones reales es normal combinar estos

Al dominar los operadores de transformación, podrás manejar flujos de datos asíncronos complejos de manera intuitiva y declarativa!

Publicado bajo licencia CC-BY-4.0.