Next.js+shadcn数据表格Hook渲染数量不一致错误求助
错误原因与修复方案
错误原因
这个报错是违反React Hooks核心规则导致的:所有Hooks必须在组件顶层按固定顺序调用,不能在条件判断、循环或提前返回语句之后调用Hook。
你的代码中,useQuery之后直接通过if (isLoading)和if (error)提前返回,而useReactTable这个Hook在条件判断之后才调用。当组件从加载状态切换到加载完成状态时,第一次渲染(加载中)只执行了前面的useState和useQuery,第二次渲染(加载完成)会多执行useReactTable,导致两次渲染的Hook数量不一致,触发该错误。
另外还有两个次要问题:
- 后端接口返回的是包含
schools数组的对象,但你直接把整个data传给表格的data属性,表格无法正确识别行数据 - 过滤输入框里的
table.getColumn("email")对应列的accessorKey是admin_email,会导致过滤功能失效
修复步骤
- 将加载、错误的条件判断移到所有Hook调用之后,确保每次渲染执行相同数量的Hook
- 表格的
data参数改为data.schools,对应后端返回的数组数据 - 修正过滤输入框的列名,从
email改为admin_email
修改后的前端组件代码
"use client" import React ,{useState} from "react" import { ColumnDef, ColumnFiltersState, SortingState, VisibilityState, flexRender, getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable, } from "@tanstack/react-table" import { ArrowUpDown, ChevronDown, MoreHorizontal } from "lucide-react" import { Button } from "@/components/ui/button" import { Checkbox } from "@/components/ui/checkbox" import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu" import { Input } from "@/components/ui/input" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { useQuery } from 'react-query' export const columns: any = [ { id: "select", header: ({ table } : any) => ( <Checkbox checked={ table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && "indeterminate") } onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} aria-label="Select all" /> ), cell: ({ row } : any) => ( <Checkbox checked={row.getIsSelected()} onCheckedChange={(value) => row.toggleSelected(!!value)} aria-label="Select row" /> ), enableSorting: false, enableHiding: false, }, { accessorKey: "status", header: "Status", cell: ({ row } : any) => ( <div className="capitalize">{row.getValue("status")}</div> ), }, { accessorKey: "name", header: ({ column } : any) => { return ( <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} > Name <ArrowUpDown className="ml-2 h-4 w-4" /> </Button> ) }, cell: ({ row } : any) => <div className="lowercase">{row.getValue("name")}</div>, }, { accessorKey: "admin_email", header: ({ column } : any) => { return ( <Button variant="ghost" onClick={() => column.toggleSorting(column.getIsSorted() === "asc")} > Email <ArrowUpDown className="ml-2 h-4 w-4" /> </Button> ) }, cell: ({ row } : any) => <div className="lowercase">{row.getValue("admin_email")}</div>, }, { accessorKey: "phone", header: () => <div className="text-right">Phone</div>, cell: ({ row } : any) => { const phone = parseFloat(row.getValue("phone")) return <div className="text-right font-medium">{phone}</div> }, }, { id: "actions", enableHiding: false, cell: ({ row } : any) => { const payment = row.original return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" className="h-8 w-8 p-0"> <span className="sr-only">Open menu</span> <MoreHorizontal className="h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> <DropdownMenuLabel>Actions</DropdownMenuLabel> <DropdownMenuItem onClick={() => navigator.clipboard.writeText(payment.id)} > Copy payment ID </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem>View customer</DropdownMenuItem> <DropdownMenuItem>View payment details</DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> ) }, }, ] export function SchoolTable() { const [sorting, setSorting] = useState<SortingState>([]) const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>( [] ) const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}) const [rowSelection, setRowSelection] = useState({}) const { isLoading, error, data } : any = useQuery('repoData', () => fetch('http://localhost:5000/api/getSchools?page=1').then(res => res.json() ) ) // 所有Hook必须在条件判断之前调用 const table = useReactTable({ // 改用后端返回的schools数组 data: data?.schools || [], columns, onSortingChange: setSorting, onColumnFiltersChange: setColumnFilters, getCoreRowModel: getCoreRowModel(), getPaginationRowModel: getPaginationRowModel(), getSortedRowModel: getSortedRowModel(), getFilteredRowModel: getFilteredRowModel(), onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: setRowSelection, state: { sorting, columnFilters, columnVisibility, rowSelection, }, }) // 条件判断移到Hook调用之后 if (isLoading) return 'Loading...' if (error) return 'An error has occurred: ' + error.message return ( <div className="w-full"> <div className="flex items-center py-4"> <Input placeholder="Filter emails..." // 修正列名为admin_email value={(table.getColumn("admin_email")?.getFilterValue() as string) ?? ""} onChange={(event) => table.getColumn("admin_email")?.setFilterValue(event.target.value) } className="max-w-sm" /> <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="outline" className="ml-auto"> Columns <ChevronDown className="ml-2 h-4 w-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end"> {table .getAllColumns() .filter((column) => column.getCanHide()) .map((column) => { return ( <DropdownMenuCheckboxItem key={column.id} className="capitalize" checked={column.getIsVisible()} onCheckedChange={(value) => column.toggleVisibility(!!value) } > {column.id} </DropdownMenuCheckboxItem> ) })} </DropdownMenuContent> </DropdownMenu> </div> <div className="rounded-md border"> <Table> <TableHeader> {table.getHeaderGroups().map((headerGroup) => ( <TableRow key={headerGroup.id}> {headerGroup.headers.map((header) => { return ( <TableHead key={header.id}> {header.isPlaceholder ? null : flexRender( header.column.columnDef.header, header.getContext() )} </TableHead> ) })} </TableRow> ))} </TableHeader> <TableBody> {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( <TableRow key={row.id} data-state={row.getIsSelected() && "selected"} > {row.getVisibleCells().map((cell) => ( <TableCell key={cell.id}> {flexRender( cell.column.columnDef.cell, cell.getContext() )} </TableCell> ))} </TableRow> )) ) : ( <TableRow> <TableCell colSpan={columns.length} className="h-24 text-center" > No results. </TableCell> </TableRow> )} </TableBody> </Table> </div> <div className="flex items-center justify-end space-x-2 py-4"> <div className="flex-1 text-sm text-muted-foreground"> {table.getFilteredSelectedRowModel().rows.length} of{" "} {table.getFilteredRowModel().rows.length} row(s) selected. </div> <div className="space-x-2"> <Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()} > Previous </Button> <Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()} > Next </Button> </div> </div> </div> ) }
内容的提问来源于stack exchange,提问作者ismail taibi
相关产品推荐
相关产品推荐

