groupBy - Agrupar valores según clave
El operador groupBy agrupa los valores emitidos del stream según una clave especificada y genera cada grupo como un Observable individual. Es útil al clasificar datos por categoría o aplicar diferentes procesamientos a cada grupo.
🔰 Sintaxis básica y uso
import { from } from 'rxjs';
import { groupBy, mergeMap, toArray, map } from 'rxjs';
interface Person {
name: string;
age: number;
}
const people: Person[] = [
{ name: 'Taro', age: 25 },
{ name: 'Hanako', age: 30 },
{ name: 'Jiro', age: 25 },
{ name: 'Misaki', age: 30 },
{ name: 'Kenta', age: 35 },
];
from(people).pipe(
groupBy(person => person.age), // Agrupar por edad
mergeMap(group =>
group.pipe(
toArray(),
map(arr => ({ age: group.key, people: arr }))
)
)
).subscribe(result => {
console.log(`Edad ${result.age}:`, result.people);
});
// Salida:
// Edad 25: [{name: 'Taro', age: 25}, {name: 'Jiro', age: 25}]
// Edad 30: [{name: 'Hanako', age: 30}, {name: 'Misaki', age: 30}]
// Edad 35: [{name: 'Kenta', age: 35}]- Agrupar usando
groupBy(person => person.age)con la edad como clave - Cada grupo se trata como
GroupedObservable, con acceso a la clave del grupo mediante la propiedadkey - Procesar el Observable de cada grupo con
mergeMap
🌐 Documentación oficial de RxJS - groupBy
💡 Patrones de uso típicos
- Clasificación de datos por categoría
- Procesamiento de agregación por grupo
- Procesamiento por tipo de log o evento
- Agrupación y transformación de datos
🧠 Ejemplo de código práctico (con UI)
Ejemplo que agrupa por color y muestra la cantidad al hacer clic en un botón.
import { fromEvent, from } from 'rxjs';
import { groupBy, mergeMap, toArray, switchMap, map } from 'rxjs';
// Crear botones
const colors = ['rojo', 'azul', 'verde', 'amarillo'];
colors.forEach(color => {
const button = document.createElement('button');
button.textContent = color;
button.style.margin = '5px';
button.style.padding = '10px';
button.dataset.color = color;
document.body.appendChild(button);
});
const calculateButton = document.createElement('button');
calculateButton.textContent = 'Calcular';
calculateButton.style.margin = '5px';
calculateButton.style.padding = '10px';
document.body.appendChild(calculateButton);
// Crear área de salida
const output = document.createElement('div');
output.style.marginTop = '10px';
output.style.fontFamily = 'monospace';
document.body.appendChild(output);
// Registrar colores en los que se hizo clic
const clicks: string[] = [];
// Evento de clic en botones de color
fromEvent(document, 'click').subscribe((event: Event) => {
const target = event.target as HTMLElement;
const color = target.dataset.color;
if (color) {
clicks.push(color);
output.innerHTML = `Colores seleccionados: ${clicks.join(', ')}`;
}
});
// Agrupar y mostrar al hacer clic en el botón de cálculo
fromEvent(calculateButton, 'click').pipe(
switchMap(() =>
from(clicks).pipe(
groupBy(color => color),
mergeMap(group =>
group.pipe(
toArray(),
map(items => ({ color: group.key, count: items.length }))
)
),
toArray()
)
)
).subscribe(results => {
if (results.length === 0) {
output.innerHTML = '<p>Aún no se han seleccionado colores</p>';
return;
}
const resultText = results
.map(r => `${r.color}: ${r.count} veces`)
.join('<br>');
output.innerHTML = `<h3>Resultados del cálculo</h3>${resultText}`;
});- Hacer clic en los botones de color para seleccionar colores
- Agrupar por color con el botón "Calcular" y mostrar la cantidad
- Agrupar por color con
groupByy contar los elementos de cada grupo
🎯 Ejemplo de agregación por categoría
Ejemplo que clasifica productos por categoría y calcula el total por categoría.
import { from } from 'rxjs';
import { groupBy, mergeMap, reduce, map } from 'rxjs';
interface Product {
name: string;
category: string;
price: number;
}
const products: Product[] = [
{ name: 'manzana', category: 'frutas', price: 150 },
{ name: 'naranja', category: 'frutas', price: 100 },
{ name: 'zanahoria', category: 'verduras', price: 80 },
{ name: 'tomate', category: 'verduras', price: 120 },
{ name: 'leche', category: 'lácteos', price: 200 },
{ name: 'queso', category: 'lácteos', price: 300 },
];
from(products).pipe(
groupBy(product => product.category),
mergeMap(group =>
group.pipe(
reduce((total, product) => total + product.price, 0),
map(total => ({ category: group.key, total }))
)
)
).subscribe(result => {
console.log(`${result.category}: ${result.total} yenes`);
});
// Salida:
// frutas: 250 yenes
// verduras: 200 yenes
// lácteos: 500 yenes🎯 Ejemplo de uso del selector de elementos
También se pueden transformar valores al agrupar.
import { from } from 'rxjs';
import { groupBy, map, mergeMap, toArray } from 'rxjs';
interface Student {
name: string;
grade: number;
score: number;
}
const students: Student[] = [
{ name: 'Taro', grade: 1, score: 85 },
{ name: 'Hanako', grade: 2, score: 92 },
{ name: 'Jiro', grade: 1, score: 78 },
{ name: 'Misaki', grade: 2, score: 88 },
];
from(students).pipe(
groupBy(
student => student.grade, // Selector de clave
student => student.name // Selector de elemento (mantener solo el nombre)
),
mergeMap(group =>
group.pipe(
toArray(),
map(names => ({ grade: group.key, students: names }))
)
)
).subscribe(result => {
console.log(`Grado ${result.grade}:`, result.students.join(', '));
});
// Salida:
// Grado 1: Taro, Jiro
// Grado 2: Hanako, Misaki- Primer argumento: selector de clave (criterio de agrupación)
- Segundo argumento: selector de elemento (valor a guardar en el grupo)
🎯 Uso de groupBy con seguridad de tipos
Ejemplo aprovechando la inferencia de tipos de TypeScript.
import { from } from 'rxjs';
import { groupBy, mergeMap, toArray, map } from 'rxjs';
type LogLevel = 'info' | 'warning' | 'error';
interface LogEntry {
level: LogLevel;
message: string;
timestamp: number;
}
const logs: LogEntry[] = [
{ level: 'info', message: 'Inicio de aplicación', timestamp: 1000 },
{ level: 'warning', message: 'Mensaje de advertencia', timestamp: 2000 },
{ level: 'error', message: 'Error ocurrido', timestamp: 3000 },
{ level: 'info', message: 'Procesamiento completo', timestamp: 4000 },
{ level: 'error', message: 'Error de conexión', timestamp: 5000 },
];
from(logs).pipe(
groupBy(log => log.level),
mergeMap(group =>
group.pipe(
toArray(),
map(entries => ({
level: group.key,
count: entries.length,
messages: entries.map(e => e.message)
}))
)
)
).subscribe(result => {
console.log(`[${result.level.toUpperCase()}] ${result.count} elementos`);
result.messages.forEach(msg => console.log(` - ${msg}`));
});
// Salida:
// [INFO] 2 elementos
// - Inicio de aplicación
// - Procesamiento completo
// [WARNING] 1 elemento
// - Mensaje de advertencia
// [ERROR] 2 elementos
// - Error ocurrido
// - Error de conexión🎯 Aplicar diferentes procesamientos a cada grupo
Ejemplo aplicando diferentes procesamientos a cada grupo.
import { from, of } from 'rxjs';
import { groupBy, mergeMap, delay, map } from 'rxjs';
interface Task {
id: number;
priority: 'high' | 'medium' | 'low';
name: string;
}
const tasks: Task[] = [
{ id: 1, priority: 'high', name: 'Tarea urgente' },
{ id: 2, priority: 'low', name: 'Tarea para después' },
{ id: 3, priority: 'high', name: 'Tarea importante' },
{ id: 4, priority: 'medium', name: 'Tarea normal' },
];
from(tasks).pipe(
groupBy(task => task.priority),
mergeMap(group => {
// Establecer tiempo de retardo según prioridad
const delayTime =
group.key === 'high' ? 0 :
group.key === 'medium' ? 1000 :
2000;
return group.pipe(
delay(delayTime),
map(task => ({ ...task, processedAt: Date.now() }))
);
})
).subscribe(task => {
console.log(`[${task.priority}] Procesar ${task.name}`);
});
// Salida (en orden de prioridad):
// [high] Procesar Tarea urgente
// [high] Procesar Tarea importante
// (después de 1 segundo)
// [medium] Procesar Tarea normal
// (después de 1 segundo adicional)
// [low] Procesar Tarea para después⚠️ Puntos de atención
Gestión de suscripción de Observables de grupo
groupBy crea un Observable para cada grupo. Estos Observables pueden causar fugas de memoria si no se suscriben adecuadamente.
// ❌ Mal ejemplo: no suscribirse a los Observables de grupo
from([1, 2, 3, 4, 5]).pipe(
groupBy(n => n % 2 === 0 ? 'even' : 'odd')
).subscribe(group => {
// No se suscribe al Observable del grupo
console.log('Grupo:', group.key);
});Solución: Asegurarse de procesar cada grupo con mergeMap, concatMap, switchMap, etc.
import { from } from 'rxjs';
import { groupBy, mergeMap, toArray } from 'rxjs';
// ✅ Buen ejemplo: procesar cada grupo adecuadamente
from([1, 2, 3, 4, 5]).pipe(
groupBy(n => n % 2 === 0 ? 'even' : 'odd'),
mergeMap(group =>
group.pipe(toArray())
)
).subscribe(console.log);Generación dinámica de grupos
Se crea un nuevo Observable de grupo cada vez que aparece una nueva clave. Se requiere precaución cuando hay muchos tipos de claves.
// Ejemplo donde los tipos de claves pueden aumentar infinitamente
fromEvent(document, 'click').pipe(
groupBy(() => Math.random()) // Clave diferente cada vez
).subscribe(); // Peligro de fuga de memoria📚 Operadores relacionados
partition- Dividir en dos Observables según condiciónreduce- Obtener resultado final de agregaciónscan- Agregación acumulativatoArray- Reunir todos los valores en un array
Resumen
El operador groupBy agrupa los valores del stream según una clave y puede tratar cada grupo como un Observable individual. Es muy útil para el procesamiento complejo de datos como clasificación de datos, agregación por categoría y procesamiento diferente por grupo. Sin embargo, cada Observable de grupo debe suscribirse adecuadamente, y normalmente se usa en combinación con mergeMap u otros operadores.