Tanstack Table v8全量行展开/折叠按钮渲染失败求助
解决TanStack Table v8全量展开/折叠按钮及列头函数不渲染问题
核心问题分析
- 列头函数未渲染:直接调用
header.column.columnDef.header无法执行函数式表头,TanStack Table需要通过flexRender来渲染函数式的表头/单元格内容,否则函数会被当作普通值处理。 - 闭包与依赖错误:
columns的useMemo依赖为空数组,但表头函数引用了后续创建的table实例,导致引用异常且无法获取最新的table状态。 - expanded状态类型错误:将
expanded初始化为布尔值false不符合规范,TanStack Table v8的expanded状态是记录行ID的对象(如{rowId: true}),布尔值会导致展开功能异常。
修复后的完整代码
"use client"; import { flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, getExpandedRowModel, useReactTable, createColumnHelper, } from "@tanstack/react-table"; import Image from "next/image"; import { useState, useMemo } from "react"; import useSWR from "swr"; import JobsFilterBox from "./JobsFilterBox"; const fetcher = (...args) => fetch(...args).then((res) => res.json()); const columnHelper = createColumnHelper(); const JobsTable = () => { const [columnFilters, setColumnFilters] = useState([]); // 修正expanded初始状态为对象类型 const [expanded, setExpanded] = useState({}); const { data: jobsData, error: jobsError, isLoading: jobsIsLoading, } = useSWR("http://localhost:9600/Job/jobs", fetcher); // 调整columns的依赖,表头函数通过context获取table实例 const columns = useMemo( () => [ columnHelper.accessor("expander", { id: "expander", size: 20, enableSorting: false, // 从header context中获取table实例,避免闭包问题 header: ({ table }) => ( <button onClick={table.getToggleAllRowsExpandedHandler()}> {table.getIsAllRowsExpanded() ? "-" : "+"} </button> ), cell: (info) => { return ( <div className="relative"> {info.row.getCanExpand() && ( <button onClick={info.row.getToggleExpandedHandler()}> <Image src="/table_expander.svg" fill={true} alt="table expander" className={` ${info.row.getIsExpanded() ? "rotate-180 " : "rotate-0 "}`} /> </button> )} </div> ); }, }), columnHelper.accessor("Serial", { header: "Serial", cell: (props) => <p>{props.getValue()}</p>, size: 80, }), // 其他列定义... ], // 加入jobsData作为依赖,确保数据更新时columns重新计算 [jobsData], ); const table = useReactTable({ data: jobsData || [], // 处理数据未加载时的空值 columns, state: { columnFilters, expanded, }, getCoreRowModel: getCoreRowModel(), getExpandedRowModel: getExpandedRowModel(), getSortedRowModel: getSortedRowModel(), onExpandedChange: setExpanded, getSubRows: (row) => row.Tasks || [], // 处理无Tasks的边界情况 getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), initialState: { pagination: { pageSize: 27, }, }, columnResizeMode: "onChange", }); if (jobsError) return <div>加载失败</div>; if (jobsIsLoading) return <div>加载中...</div>; return ( <div className="relative grid h-screen w-full grid-cols-[0.5fr_1fr] grid-rows-[40px_700px_80px] gap-y-1"> <JobsFilterBox className="col-[1_/_3] row-[1_/_2]" columnFilters={columnFilters} setColumnFilters={setColumnFilters} /> <div className="col-[1_/_3] row-[2_/_3] overflow-x-scroll"> <table className="border-l border-t border-gray-300 bg-white text-left text-xs" style={{ width: table.getTotalSize() }} > <thead> {table.getHeaderGroups().map((headerGroup) => ( <tr className="relative flex" key={headerGroup.id}> {headerGroup.headers.map((header) => ( <th className="relative flex items-center justify-center border-b border-r border-gray-300 bg-[#2170ba] p-2 text-center text-gray-100" style={{ width: header.getSize() }} key={header.id} > {/* 使用flexRender渲染表头,支持函数式定义 */} {flexRender(header.column.columnDef.header, header.getContext())} {header.column.getCanSort() && ( <div className="relative ml-2 h-3 w-3 cursor-pointer"> <Image fill={true} src="../sort_icon.svg" alt="sort icon" onClick={header.column.getToggleSortingHandler()} /> </div> )} <div onMouseDown={header.getResizeHandler()} onTouchStart={header.getResizeHandler()} className={`${header.column.getIsResizing() ? "bg-orange-600 opacity-100" : ""} absolute right-0 top-0 h-full w-[5px] cursor-col-resize touch-none select-none bg-blue-900 opacity-0 hover:opacity-100`} ></div> </th> ))} </tr> ))} </thead> <tbody className=""> {table.getRowModel().rows.map((row) => ( <tr className={` relative flex h-6 ${row.depth > 0 ? "bg-[#afd6f4] text-gray-700" : "bg-[#93c8f0] text-gray-700"} ${row.depth === 1 && row.id.includes(".0") ? "border-t border-gray-400" : ""} `} key={row.id} > {row.getVisibleCells().map((cell) => ( <td className={`truncate border-b border-r border-gray-300 ${cell.column.id === "State" ? getStatusColor(row.original.Status) : ""} `} style={{ width: cell.column.getSize() }} key={cell.id} > {flexRender(cell.column.columnDef.cell, cell.getContext())} </td> ))} </tr> ))} </tbody> </table> </div> <div className="col-span-2 col-start-1 row-[3_/_4] flex flex-[1_0] flex-row flex-nowrap"> <div className=""> <button className="mr-2 w-fit shrink-0" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} > {"<"} </button> </div> <div className=""> <p className="w-fit text-sm text-gray-900"> 第 {table.getState().pagination.pageIndex + 1} 页,共 {table.getPageCount()} 页 </p> </div> <div className=""> <button className="ml-2 w-fit" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} > {">"} </button> </div> </div> </div> ); }; // 补充getStatusColor函数,避免未定义报错 const getStatusColor = (status) => { switch(status) { case 'Completed': return 'bg-green-100'; case 'Failed': return 'bg-red-100'; case 'Running': return 'bg-yellow-100'; default: return ''; } };
关键修复点
- 用flexRender渲染表头:替换直接调用表头定义的方式,确保函数式表头被正确执行渲染。
- 修正expanded状态类型:改为对象类型,符合TanStack Table的状态管理规范。
- 解决闭包问题:表头函数通过context获取table实例,同时更新
useMemo依赖,确保columns随数据和table状态更新。 - 边界情况处理:添加空数据判断,避免数据未加载时出现报错。
内容的提问来源于stack exchange,提问作者baerlein
相关产品推荐
相关产品推荐

