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

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 '';
  }
};

关键修复点

  1. 用flexRender渲染表头:替换直接调用表头定义的方式,确保函数式表头被正确执行渲染。
  2. 修正expanded状态类型:改为对象类型,符合TanStack Table的状态管理规范。
  3. 解决闭包问题:表头函数通过context获取table实例,同时更新useMemo依赖,确保columns随数据和table状态更新。
  4. 边界情况处理:添加空数据判断,避免数据未加载时出现报错。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 23:42:33