Al trabajar con tablas de datos en React utilizando Ant Design (antd), es común enfrentar el requisito de agrupar visualmente filas que comparten un mismo valor en una columna específica. Cuando los datos provienen de una API y están sujetos a filtros o búsquedas, la estructura es dinámica. Por lo tanto, definir uniones de celdas de forma estática no es viable. La solución fundamental consiste en preprocesar el origen de datos para calcular la propiedad rowSpan de cada registro antes de la renderización.
1. Fusión estática de filas (Concepto básico)
Para comprender el mecanismo, primero observemos cómo el componente Table utiliza la propiedad rowSpan dentro de la configuración de onCell. La regla es sancilla: la primera fila del grupo debe indicar el número total de filas a fusionar, mientras que las filas subsiguientes del mismo grupo deben tener un rowSpan de 0 para evitar que se rendericen celdas duplicadas y se rompa el diseño de la tabla.
import React, { useEffect, useState } from 'react';
import { Table } from 'antd';
const StaticMergeTable = () => {
const [dataSource, setDataSource] = useState([]);
const columns = [
{
title: 'Departamento',
dataIndex: 'department',
onCell: (_, index) => {
// Fusionar las filas 0 y 1
if (index === 0) return { rowSpan: 2 };
if (index === 1) return { rowSpan: 0 };
// Fusionar las filas 2, 3 y 4
if (index === 2) return { rowSpan: 3 };
if (index === 3 || index === 4) return { rowSpan: 0 };
},
},
{ title: 'Empleado', dataIndex: 'employee' },
{ title: 'Rol', dataIndex: 'role' },
];
useEffect(() => {
setDataSource([
{ department: 'Ingeniería', employee: 'Ana', role: 'Backend' },
{ department: 'Ingeniería', employee: 'Luis', role: 'Frontend' },
{ department: 'Diseño', employee: 'Carla', role: 'UI/UX' },
{ department: 'Diseño', employee: 'Pedro', role: 'Gráfico' },
{ department: 'Diseño', employee: 'Lucía', role: 'Animación' },
]);
}, []);
return (
<Table
bordered
rowKey="employee"
columns={columns}
dataSource={dataSource}
pagination={false}
/>
);
};
export default StaticMergeTable;
2. Fusión dinámica de filas (Algoritmo de preprocesamiento)
En un escenario real, los datos son variables. El objetivo es recorrer el arreglo de datos, identificar bloques consecutivos que comparten el mismo valor en la columna objetivo, y asignar dinámicamente el valor de rowSpan. El primer elemento del bloque obtiene el tamaño total del grupo, y los elementos restantes se marcan con 0.
import React, { useEffect, useState } from 'react';
import { Table } from 'antd';
const DynamicMergeTable = () => {
const [tableRecords, setTableRecords] = useState([]);
const columns = [
{
title: 'Departamento',
dataIndex: 'department',
onCell: (record) => ({ rowSpan: record.rowSpan }),
},
{ title: 'Empleado', dataIndex: 'employee' },
{ title: 'Rol', dataIndex: 'role' },
];
// Algoritmo para calcular los rowSpan dinámicamente
const computeRowSpans = (data, mergeKey) => {
if (!data || data.length === 0) return [];
const processedData = [...data];
let i = 0;
while (i < processedData.length) {
let groupSize = 1;
// Contar elementos consecutivos con el mismo valor en mergeKey
while (
i + groupSize < processedData.length &&
processedData[i][mergeKey] === processedData[i + groupSize][mergeKey]
) {
groupSize++;
}
// Asignar el tamaño del grupo al primer elemento y 0 a los demás
processedData[i].rowSpan = groupSize;
for (let j = 1; j < groupSize; j++) {
processedData[i + j].rowSpan = 0;
}
// Avanzar al siguiente grupo
i += groupSize;
}
return processedData;
};
useEffect(() => {
const apiResponse = [
{ department: 'Ingeniería', employee: 'Ana', role: 'Backend' },
{ department: 'Ingeniería', employee: 'Luis', role: 'Frontend' },
{ department: 'Diseño', employee: 'Carla', role: 'UI/UX' },
{ department: 'Diseño', employee: 'Pedro', role: 'Gráfico' },
{ department: 'Diseño', employee: 'Lucía', role: 'Animación' },
{ department: 'Marketing', employee: 'Sofía', role: 'SEO' },
];
const formattedData = computeRowSpans(apiResponse, 'department');
setTableRecords(formattedData);
}, []);
return (
<Table
bordered
rowKey="employee"
columns={columns}
dataSource={tableRecords}
pagination={false}
/>
);
};
export default DynamicMergeTable;