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

TanStack Table:实现点击仅展开单个单元格而非整行

TanStack Table 实现行内单元格独立展开

问题背景

使用TanStack Table时,同一行内多个单元格都配置了展开按钮,点击任意一个按钮会触发整行所有可展开单元格同步展开/折叠,需要实现点击单个单元格的按钮仅控制该单元格的展开状态,不影响其他单元格。

解决方案

TanStack Table默认的行展开机制是基于行级别的状态管理,要实现单元格独立展开,需要自定义单元格级别的展开状态来实现独立控制:

步骤1:添加单元格级别的展开状态

在ExpandableTable组件中新增状态,存储每个行+列组合的展开状态:

const [columnExpandedState, setColumnExpandedState] = useState<Record<string, boolean>>({});

状态键格式为${rowId}_${columnId},用于唯一标识每个单元格的展开状态。

步骤2:自定义展开切换函数

创建处理单个单元格展开/折叠的函数,针对指定行和列更新状态:

const toggleColumnExpanded = (rowId: string, columnId: string) => {
  const key = `${rowId}_${columnId}`;
  setColumnExpandedState(prev => ({
    ...prev,
    [key]: !prev[key],
  }));
};

步骤3:修改单元格按钮的点击事件

在列定义的cell渲染函数中,替换默认的row.getToggleExpandedHandler()为自定义函数,同时根据单元格的展开状态显示对应图标:

// 以firstName列为例,lastName列同理
cell: ({ row, getValue, column, columnExpandedState, toggleColumnExpanded }) => (
  <div style={{ paddingLeft: `${row.depth * 2}rem` }}>
    {row.getCanExpand() ? (
      <button
        onClick={() => toggleColumnExpanded(row.id, column.id)}
        style={{ cursor: "pointer" }}
      >
        {columnExpandedState[`${row.id}_${column.id}`] ? "👇" : "👉"}
      </button>
    ) : (
      "🔵"
    )}
    {getValue<string>()}
  </div>
),

步骤4:调整展开内容的渲染逻辑

为每个单元格展示独立的展开内容,修改表格的tbody部分,根据对应单元格的展开状态渲染对应的子组件:

<tbody>
  {table.getRowModel().rows?.length ? (
    table.getRowModel().rows.map((row) => (
      <>
        <tr
          key={row.id}
          data-state={row.getIsSelected() && "selected"}
        >
          {row.getVisibleCells().map((cell) => (
            <td key={cell.id}>
              {flexRender(
                cell.column.columnDef.cell,
                {
                  ...cell.getContext(),
                  // 将自定义状态和函数传递给单元格
                  columnExpandedState,
                  toggleColumnExpanded,
                }
              )}
            </td>
          ))}
        </tr>
        {/* 渲染每个单元格对应的展开行 */}
        {row.getVisibleCells().map((cell) => {
          const expandKey = `${row.id}_${cell.column.id}`;
          if (columnExpandedState[expandKey]) {
            return (
              <tr key={expandKey}>
                <td colSpan={row.getVisibleCells().length}>
                  {renderSubComponent({ row, columnId: cell.column.id })}
                </td>
              </tr>
            );
          }
          return null;
        })}
      </>
    ))
  ) : (
    <tr>
      <td colSpan={columns.length} className="h-24 text-center">
        No results.
      </td>
    </tr>
  )}
</tbody>

修改后的完整代码

App.tsx

import {
  ColumnDef,
  VisibilityState,
  flexRender,
  getCoreRowModel,
  getExpandedRowModel,
  useReactTable,
  Row,
} from "@tanstack/react-table";
import { useState } from "react";

import { expandableColumn, expandableData } from "./column-data";

interface Person {
  firstName: string;
  lastName: string;
  age: number;
  visits: number;
  progress: number;
  status: string;
}

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
  renderSubComponent: (props: { row: Row<TData>; columnId: string }) => React.ReactElement;
  getRowCanExpand: (row: Row<TData>) => boolean;
}

const renderSubComponent = ({ row, columnId }: { row: Row<Person>; columnId: string }) => {
  return (
    <pre style={{ fontSize: "10px" }}>
      <code>{`${columnId} 展开内容:\n${JSON.stringify(row.original, null, 2)}`}</code>
    </pre>
  );
};

export function ExpandableTable<TData, TValue>({
  columns,
  data,
  renderSubComponent,
  getRowCanExpand,
}: DataTableProps<TData, TValue>) {
  const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
  // 存储单元格级别的展开状态:rowId_columnId -> boolean
  const [columnExpandedState, setColumnExpandedState] = useState<Record<string, boolean>>({});

  // 切换单个单元格的展开状态
  const toggleColumnExpanded = (rowId: string, columnId: string) => {
    const key = `${rowId}_${columnId}`;
    setColumnExpandedState(prev => ({
      ...prev,
      [key]: !prev[key],
    }));
  };

  const table = useReactTable({
    data,
    columns,
    getRowCanExpand,
    state: {
      columnVisibility,
    },
    autoResetPageIndex: false,
    enableRowSelection: true,
    onColumnVisibilityChange: setColumnVisibility,
    getCoreRowModel: getCoreRowModel(),
    getExpandedRowModel: getExpandedRowModel(),
  });

  return (
    <div className="space-y-4">
      <div className="rounded-md border">
        <table>
          <thead>
            {table.getHeaderGroups().map((headerGroup) => (
              <tr key={headerGroup.id}>
                {headerGroup.headers.map((header) => {
                  return (
                    <th key={header.id}>
                      {header.isPlaceholder
                        ? null
                        : flexRender(
                            header.column.columnDef.header,
                            header.getContext()
                          )}
                    </th>
                  );
                })}
              </tr>
            ))}
          </thead>
          <tbody>
            {table.getRowModel().rows?.length ? (
              table.getRowModel().rows.map((row) => (
                <>
                  <tr
                    key={row.id}
                    data-state={row.getIsSelected() && "selected"}
                  >
                    {row.getVisibleCells().map((cell) => (
                      <td key={cell.id}>
                        {flexRender(
                          cell.column.columnDef.cell,
                          {
                            ...cell.getContext(),
                            columnExpandedState,
                            toggleColumnExpanded,
                          }
                        )}
                      </td>
                    ))}
                  </tr>
                  {/* 渲染每个单元格对应的展开行 */}
                  {row.getVisibleCells().map((cell) => {
                    const expandKey = `${row.id}_${cell.column.id}`;
                    if (columnExpandedState[expandKey]) {
                      return (
                        <tr key={expandKey}>
                          <td colSpan={row.getVisibleCells().length}>
                            {renderSubComponent({ row, columnId: cell.column.id })}
                          </td>
                        </tr>
                      );
                    }
                    return null;
                  })}
                </>
              ))
            ) : (
              <tr>
                <td colSpan={columns.length} className="h-24 text-center">
                  No results.
                </td>
              </tr>
            )}
          </tbody>
        </table>
      </div>
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <h1>Expand Table Example</h1>
      <h2>Start editing to see some magic happen!</h2>
      <ExpandableTable
        data={expandableData}
        columns={expandableColumn}
        renderSubComponent={renderSubComponent}
        getRowCanExpand={() => true}
      />
    </div>
  );
}

column-data.tsx

import { ColumnDef } from "@tanstack/react-table";

export const expandableColumn: ColumnDef<any>[] = [
  {
    accessorKey: "firstName",
    header: "First Name",
    cell: ({ row, getValue, column, columnExpandedState, toggleColumnExpanded }) => (
      <div
        style={{
          paddingLeft: `${row.depth * 2}rem`,
        }}
      >
        {row.getCanExpand() ? (
          <button
            onClick={() => toggleColumnExpanded(row.id, column.id)}
            style={{ cursor: "pointer" }}
          >
            {columnExpandedState[`${row.id}_${column.id}`] ? "👇" : "👉"}
          </button>
        ) : (
          "🔵"
        )}
        {getValue<string>()}
      </div>
    ),
    footer: (props) => props.column.id,
  },
  {
    accessorFn: (row) => row.lastName,
    id: "lastName",
    cell: ({ row, getValue, column, columnExpandedState, toggleColumnExpanded }) => (
      <div
        style={{
          paddingLeft: `${row.depth * 2}rem`,
        }}
      >
        {row.getCanExpand() ? (
          <button
            onClick={() => toggleColumnExpanded(row.id, column.id)}
            style={{ cursor: "pointer" }}
          >
            {columnExpandedState[`${row.id}_${column.id}`] ? "♟" : "🔦"}
          </button>
        ) : (
          "🔵"
        )}
        {getValue<string>()}
      </div>
    ),
    header: () => <span>Last Name</span>,
    footer: (props) => props.column.id,
  },
  {
    accessorKey: "age",
    header: () => "Age",
    footer: (props) => props.column.id,
  },
  {
    accessorKey: "visits",
    header: () => <span>Visits</span>,
    footer: (props) => props.column.id,
  },
  {
    accessorKey: "status",
    header: "Status",
    footer: (props) => props.column.id,
  },
  {
    accessorKey: "progress",
    header: "Profile Progress",
    footer: (props) => props.column.id,
  },
];

export const expandableData = [
  {
    firstName: "Marquis",
    lastName: "Runolfsdottir",
    age: 36,
    visits: 322,
    progress: 46,
    status: "single",
  },
  {
    firstName: "Hayden",
    lastName: "Ritchie",
    age: 35,
    visits: 208,
    progress: 79,
    status: "relationship",
  },
  {
    firstName: "Destany",
    lastName: "Boehm",
    age: 37,
    visits: 752,
    progress: 97,
    status: "relationship",
  },
  {
    firstName: "Kale",
    lastName: "Johnston",
    age: 36,
    visits: 378,
    progress: 13,
    status: "complicated",
  },
  {
    firstName: "Maybell",
    lastName: "Brown",
    age: 16,
    visits: 783,
    progress: 76,
    status: "complicated",
  },
  {
    firstName: "Emelia",
    lastName: "Cronin",
    age: 30,
    visits: 655,
    progress: 96,
    status: "complicated",
  },
  {
    firstName: "Kali",
    lastName: "Marvin",
    age: 18,
    visits: 926,
    progress: 57,
    status: "complicated",
  },
  {
    firstName: "Khalid",
    lastName: "Robel",
    age: 34,
    visits: 671,
    progress: 100,
    status: "complicated",
  },
  {
    firstName: "Zena",
    lastName: "Hilll",
    age: 34,
    visits: 934,
    progress: 18,
    status: "single",
  },
  {
    firstName: "Hiram",
    lastName: "Koss",
    age: 12,
    visits: 757,
    progress: 44,
    status: "relationship",
  },
];

关键说明

  • 放弃TanStack Table默认的行级展开状态,改用自定义的单元格级状态,每个单元格的展开状态由rowId和columnId的组合唯一标识。
  • 自定义的toggleColumnExpanded函数仅更新目标单元格的状态,实现独立控制。
  • 展开内容可以根据columnId区分展示不同的内容,完全独立于其他单元格。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 21:54:56