You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React/HTML中如何更优地以表格形式展示嵌套JSON对象?

嵌套JSON转相对尺寸表格的优化实现方案

问题背景

给定嵌套JSON对象:

{
  "A":{
    "a": [1,2],
    "b": [3]
  },
  "B":{
    "c":[4]
  }
}

需要将其转换为相对尺寸的规整表格(一级键跨行对应下属子项,二级键跨行对应数组元素),但现有实现存在痛点:

  • 用div布局:无绝对尺寸时,单元格行列对齐难以保证
  • 原生table实现:需手动解构对象+编写复杂的rowspan计算逻辑,代码可读性极差

优化方案

方案1:用React声明式库简化跨行处理

推荐使用@tanstack/react-table(v8+),它支持通过配置实现跨行合并,无需手动计算rowspan,代码更具声明式特点,同时天然支持相对尺寸。

实现步骤:

  1. 安装依赖:
npm install @tanstack/react-table
  1. 组件代码:
import { useMemo } from 'react';
import { useTable, useRowSpan } from '@tanstack/react-table';

export default function NestedJsonTable() {
  // 扁平化嵌套JSON,标记层级
  const data = useMemo(() => {
    const flattened = [];
    const source = {
      "A":{ "a": [1,2], "b": [3] },
      "B":{ "c":[4] }
    };
    Object.entries(source).forEach(([parentKey, children]) => {
      Object.entries(children).forEach(([childKey, values]) => {
        values.forEach((val, idx) => {
          flattened.push({
            parent: idx === 0 ? parentKey : '',
            child: idx === 0 ? childKey : '',
            value: val
          });
        });
      });
    });
    return flattened;
  }, []);

  // 配置列规则,包含跨行合并逻辑
  const columns = useMemo(() => [
    {
      accessorKey: 'parent',
      header: '一级键',
      rowSpan: (row) => {
        const parentVal = row.original.parent;
        if (!parentVal) return 0;
        return data.filter(item => item.parent === parentVal).length;
      }
    },
    {
      accessorKey: 'child',
      header: '二级键',
      rowSpan: (row) => {
        const childVal = row.original.child;
        if (!childVal) return 0;
        return data.filter(item => item.child === childVal).length;
      }
    },
    {
      accessorKey: 'value',
      header: '值'
    }
  ], [data]);

  const table = useTable({ columns, data }, useRowSpan);
  const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = table;

  return (
    <table {...getTableProps()} style={{ width: '100%', borderCollapse: 'collapse' }}>
      <thead>
        {headerGroups.map(headerGroup => (
          <tr {...headerGroup.getHeaderGroupProps()}>
            {headerGroup.headers.map(column => (
              <th {...column.getHeaderProps()} style={{ border: '1px solid #ccc', padding: '8px' }}>
                {column.render('Header')}
              </th>
            ))}
          </tr>
        ))}
      </thead>
      <tbody {...getTableBodyProps()}>
        {rows.map(row => {
          prepareRow(row);
          return (
            <tr {...row.getRowProps()}>
              {row.getVisibleCells().map(cell => {
                const props = cell.getCellProps();
                const span = cell.column.rowSpan?.(row, cell.column);
                if (span === 0) return null;
                if (span) props.rowSpan = span;
                return (
                  <td {...props} style={{ border: '1px solid #ccc', padding: '8px' }}>
                    {cell.render('Cell')}
                  </td>
                );
              })}
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

方案2:CSS Grid实现无table对齐布局

如果不想依赖第三方库,CSS Grid可以实现天然的行列对齐,同时支持相对尺寸,代码逻辑更直观。

组件代码:

import { useMemo } from 'react';

export default function NestedJsonGrid() {
  const gridData = useMemo(() => {
    const result = [];
    let rowIndex = 0;
    const source = {
      "A":{ "a": [1,2], "b": [3] },
      "B":{ "c":[4] }
    };

    // 预处理跨行数
    const parentSpanMap = {};
    const childSpanMap = {};
    Object.entries(source).forEach(([parent, children]) => {
      let total = 0;
      Object.entries(children).forEach(([child, values]) => {
        total += values.length;
        childSpanMap[child] = values.length;
      });
      parentSpanMap[parent] = total;
    });

    // 生成网格数据
    Object.entries(source).forEach(([parent, children]) => {
      Object.entries(children).forEach(([child, values]) => {
        values.forEach(val => {
          result.push({
            parent,
            parentSpan: parentSpanMap[parent],
            child,
            childSpan: childSpanMap[child],
            value: val,
            row: rowIndex++
          });
        });
      });
    });

    // 标记是否需要渲染单元格(避免重复)
    let prevParent = '';
    let prevChild = '';
    result.forEach(item => {
      item.showParent = item.parent !== prevParent;
      item.showChild = item.child !== prevChild;
      prevParent = item.parent;
      prevChild = item.child;
    });

    return result;
  }, []);

  return (
    <div style={{
      display: 'grid',
      gridTemplateColumns: '1fr 1fr 1fr', // 三列等宽,自适应容器
      width: '100%',
      border: '1px solid #ccc'
    }}>
      {/* 表头 */}
      {['一级键', '二级键', '值'].map(title => (
        <div key={title} style={{
          border: '1px solid #ccc',
          padding: '8px',
          fontWeight: 'bold',
          textAlign: 'center'
        }}>
          {title}
        </div>
      ))}
      {/* 内容单元格 */}
      {gridData.map((item, idx) => (
        <>
          {item.showParent && (
            <div style={{
              border: '1px solid #ccc',
              padding: '8px',
              gridRow: `${item.row + 2} / span ${item.parentSpan}` // 表头占1行,从第row+2行开始跨行
            }}>
              {item.parent}
            </div>
          )}
          {item.showChild && (
            <div style={{
              border: '1px solid #ccc',
              padding: '8px',
              gridRow: `${item.row + 2} / span ${item.childSpan}`
            }}>
              {item.child}
            </div>
          )}
          <div style={{ border: '1px solid #ccc', padding: '8px' }}>
            {item.value}
          </div>
        </>
      ))}
    </div>
  );
}

方案3:原生table的简化计算方式

如果坚持使用原生table,可以通过预处理跨行映射表,拆分计算逻辑,让代码更易读:

组件代码:

import { useMemo } from 'react';

export default function NestedJsonNativeTable() {
  const tableRows = useMemo(() => {
    const rows = [];
    const source = {
      "A":{ "a": [1,2], "b": [3] },
      "B":{ "c":[4] }
    };

    // 第一步:计算所有层级的跨行数
    const parentSpanMap = {};
    const childSpanMap = {};
    Object.entries(source).forEach(([parent, children]) => {
      let totalRows = 0;
      Object.entries(children).forEach(([child, values]) => {
        totalRows += values.length;
        childSpanMap[child] = values.length;
      });
      parentSpanMap[parent] = totalRows;
    });

    // 第二步:生成表格行,标记是否显示单元格
    let currentParent = '';
    let currentChild = '';
    Object.entries(source).forEach(([parent, children]) => {
      Object.entries(children).forEach(([child, values]) => {
        values.forEach(val => {
          rows.push({
            showParent: parent !== currentParent,
            parent,
            parentSpan: parentSpanMap[parent],
            showChild: child !== currentChild,
            child,
            childSpan: childSpanMap[child],
            value: val
          });
          currentParent = parent;
          currentChild = child;
        });
      });
    });

    return rows;
  }, []);

  return (
    <table style={{ width: '100%', borderCollapse: 'collapse' }}>
      <thead>
        <tr>
          <th style={{ border: '1px solid #ccc', padding: '8px' }}>一级键</th>
          <th style={{ border: '1px solid #ccc', padding: '8px' }}>二级键</th>
          <th style={{ border: '1px solid #ccc', padding: '8px' }}>值</th>
        </tr>
      </thead>
      <tbody>
        {tableRows.map(row => (
          <tr key={row.value}>
            {row.showParent && (
              <td style={{ border: '1px solid #ccc', padding: '8px' }} rowSpan={row.parentSpan}>
                {row.parent}
              </td>
            )}
            {row.showChild && (
              <td style={{ border: '1px solid #ccc', padding: '8px' }} rowSpan={row.childSpan}>
                {row.child}
              </td>
            )}
            <td style={{ border: '1px solid #ccc', padding: '8px' }}>{row.value}</td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}

内容的提问来源于stack exchange,提问作者JH Lee

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.05 06:09:57