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
相关产品推荐
相关产品推荐

