提问人:Kieran Corkin 提问时间:11/15/2023 更新时间:11/15/2023 访问量:22
错误:呈现的钩子比预期的要少。这可能是由意外的提前退货声明引起的。反应查询
Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement. React-Query
问:
因此,我目前正在尝试使用来自后端的一些数据将表格呈现到页面上,以显示某种市场,但我似乎遇到了这个非常令人沮丧的错误,信息不多。我也在努力诊断这个问题。这似乎也不一致。它大部分时间都显示,但有时似乎不显示。请参阅下面的错误:
页面.tsx
"use client";
import React, { useEffect } from "react";
import Card, { CardHeader } from "@/components/global/Card";
import { ILead, IMortgageLead } from "@/interfaces/ILead";
import useAdvisor from "@/context/AdvisorContext";
import { AdvisorStatus } from "@/enums/AdvisorStatus";
import Locked from "@/components/portal/Locked";
import Table, { ExpanderIcon } from "@/components/portal/Table";
import Modal from "@/components/global/Modal";
import SelectField from "@/components/portal/Forms/SelectField";
import { FaBell, FaCoins, FaFilter, FaStar } from "react-icons/fa";
import InputField from "@/components/portal/Forms/InputField";
import { CellContext, Row } from "@tanstack/react-table";
import { trpc } from "@/app/api/_trpc/client";
import { CreditStatus } from "@/enums/CreditStatus";
import { LeadStatus } from "@/enums/LeadStatus";
import InformationIcon from "@/components/global/InformationIcon";
import TableSubComponent from "@/components/portal/Marketplace/TableSubComponent";
import { formatDistance } from "date-fns";
import FilterModal from "@/components/portal/Marketplace/FilterModal";
import ErrorLock from "@/components/portal/ErrorLock";
import PurchaseLead from "@/components/portal/Marketplace/PurchaseLead";
export default function Page() {
const advisor = useAdvisor();
const [selectedPurchaseLead, setSelectedPurchaseLead] = React.useState<CellContext<IMortgageLead, any> | null>(null);
const [openFilters, setOpenFilters] = React.useState(false);
const [openCreateNotification, setOpenCreateNotification] = React.useState(false);
const [query, setQuery] = React.useState({
page: '1',
page_size: '10',
ordering: 'any'
});
const queryRsp = trpc.leads.getLeads.useQuery(query, {
refetchIntervalInBackground: false,
refetchInterval: false,
keepPreviousData: false,
});
if (
!advisor?.verified ||
advisor?.status === AdvisorStatus.FLAGGED ||
advisor?.status === AdvisorStatus.BANNED
) {
return <Locked advisor={advisor} />;
}
if (queryRsp.isError) {
return <ErrorLock />
}
if (queryRsp?.data?.count === 0) {
return <Locked title="No leads available" message="There are currently no leads available for sale, please come back later or ensure you have notifications set to recieve emails for new leads" />
}
// eslint-disable-next-line react-hooks/rules-of-hooks
const columns = React.useMemo(() => [
{
id: "expander",
header: () => null,
cell: ({ row }: CellContext<IMortgageLead, any>) => {
return (
<button
{...{
onClick:
row.original.lead_status != "Sold"
? row.getToggleExpandedHandler()
: () => null,
style: {
cursor: row.original.lead_status != "Sold" ? "pointer" : "not-allowed",
height: '100%'
},
}}
>
<ExpanderIcon
open={row.getIsExpanded()}
classNames={{
"bg-red-500": !row.getCanExpand(),
}}
/>
</button>
)
},
},
{
header: "Mortgage value",
accessorKey: "mortgage_value",
cell: (cell: CellContext<IMortgageLead, any>) => {
return (
<>
{new Intl.NumberFormat("en-GB", {
style: "currency",
currency: "GBP",
})
.format(cell.getValue().replace(',', ''))
.replace(".00", "")}{" "}
</>
);
},
},
{
header: "LTV",
accessorKey: "ltv",
cell: (cell: CellContext<IMortgageLead, any>) => {
return <>{cell.getValue()}%</>;
},
},
{
header: "Type",
accessorKey: "mortgage_type",
},
{
header: "Purpose",
accessorKey: "purpose",
},
{
header: "Location",
accessorKey: "location",
},
{
header: "Credit Status",
accessorKey: "credit_status",
cell: (cell: CellContext<IMortgageLead, any>) => {
switch (cell.getValue()) {
case CreditStatus.UNAVAILABLE:
return cell.getValue();
case CreditStatus.BAD:
case CreditStatus.POOR:
return <span className={"text-red-500"}>{cell.getValue()}</span>;
case CreditStatus.FAIR:
return <span className={"text-yellow-500"}>{cell.getValue()}</span>;
case CreditStatus.GOOD:
case CreditStatus.EXCELLENT:
return <span className={"text-emerald-500"}>{cell.getValue()}</span>;
default:
return cell.getValue();
}
},
},
{
header: "Qualified",
accessorKey: "qualified",
cell: (cell: CellContext<IMortgageLead, any>) => {
return (
<>
{cell.getValue() ? (
<span className={"text-green-500"}>Yes</span>
) : (
<span className={"text-red-500"}>No</span>
)}
</>
);
},
},
{
header: () => (
<div className={"flex items-center gap-2"}>
Deal Stage
<InformationIcon text={"this is the deal stage"} />
</div>
),
accessorKey: "qualification_type",
cell: (cell: CellContext<IMortgageLead, any>) => {
const array = Array.from(Array(cell.getValue() == 0 ? 1 : cell.getValue()).keys());
return (
<div className={"flex gap-1 text-yellow-500"}>
{array.map((item) => (
<FaStar key={item} />
))}
</div>
);
},
},
{
header: "Status",
accessorKey: "status",
cell: (cell: CellContext<IMortgageLead, any>) => {
switch (cell.getValue()) {
case LeadStatus.READY:
return (
<span
className={
"block w-full rounded-2xl bg-emerald-500 px-4 py-2 text-center font-semibold text-white"
}
>
Available
</span>
);
case LeadStatus.SOLD:
case LeadStatus.EXPIRED:
return (
<span
className={
"block w-full rounded-2xl bg-red-500 px-4 py-2 text-center font-semibold text-white "
}
>
{cell.getValue()}
</span>
);
case LeadStatus.CALLING:
return (
<span
className={
"block w-full animate-pulse rounded-2xl bg-deep-sapphire-1000 px-4 py-2 text-center font-semibold text-white"
}
>
{cell.getValue()}
</span>
);
case LeadStatus.INCOMING:
return (
<span
className={
"block w-full animate-pulse rounded-2xl bg-gray-100 px-4 py-2 text-center font-semibold"
}
>
{cell.getValue()}
</span>
);
default:
return cell.getValue();
}
},
},
{
header: "Generated",
accessorKey: "created_at",
cell: (cell: CellContext<IMortgageLead, any>) => {
return (
<span
className={
cell.row.original.qualified
? "text-emerald-500"
: "text-deep-sapphire-1000"
}
>
{formatDistance(new Date(cell.getValue()), new Date(), {addSuffix: false})}
</span>
);
},
},
{
header: "Notes",
accessorKey: "notes",
cell: (cell: CellContext<IMortgageLead, any>) => {
if (!cell.getValue()) {
return null;
}
return (
<button
className={
"block w-full rounded-2xl bg-yellow-500 px-4 py-2 text-center font-semibold"
}
>
View notes
</button>
);
},
},
{
header: "Price",
accessorKey: "price",
cell: (cell: CellContext<IMortgageLead, any>) => {
return (
<div className={"flex items-center gap-2"}>
<span className={"text-yellow-500"}>
<FaCoins />
</span>
{cell.getValue()}
</div>
);
},
},
{
header: "Actions",
cell: (cell: CellContext<IMortgageLead, any>) => {
return (
<button
onClick={(e) => {
e.preventDefault();
setSelectedPurchaseLead(cell)
}}
className={
"block w-full rounded-2xl bg-deep-sapphire-800 px-4 py-2 text-center font-semibold text-white"
}
>
Buy
</button>
);
},
},
], []);
return (
<Card
className={"p-0"}
title={
<div className={"flex items-center justify-between"}>
<CardHeader title={"Leads available for purchase"} />
<div className={"flex gap-2"}>
<button
onClick={() => {
setOpenCreateNotification(true);
}}
className={
"flex items-center gap-2 rounded-md bg-gray-100 px-3 py-2 text-sm font-semibold text-deep-sapphire-1000 shadow-sm hover:bg-gray-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300"
}
>
<FaBell /> <span className={"hidden sm:block"}>Notify me</span>
</button>
<button
onClick={() => {
setOpenFilters(true);
}}
className={
"flex items-center gap-2 rounded-md bg-gray-100 px-3 py-2 text-sm font-semibold text-deep-sapphire-1000 shadow-sm hover:bg-gray-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-300"
}
>
<FaFilter /> <span className={"hidden sm:block"}>Filter</span>
</button>
</div>
</div>
}
>
<Table
query={queryRsp}
data={queryRsp?.data ?? null}
columns={columns}
getRowCanExpand={(row) => row.original.lead_status != 'Sold'}
pagination={{
pageIndex: parseInt(query.page),
pageSize: parseInt(query.page_size),
}}
onPaginationChange={(page) => {
setQuery({
...query,
page: page.pageIndex.toString(),
page_size: page.pageSize.toString(),
});
}}
renderSubComponent={({ row }: { row: Row<IMortgageLead> }) => {
return <TableSubComponent id={row.original.id} />;
}}
/>
<Modal open={!!selectedPurchaseLead} onClose={() => {
setSelectedPurchaseLead(null)
}} title="Confirm Purchase">
<PurchaseLead cell={selectedPurchaseLead} closeModal={() => {
setSelectedPurchaseLead(null);
queryRsp.refetch();
}} />
</Modal>
<Modal
open={openCreateNotification}
onClose={() => {
setOpenCreateNotification(false);
}}
title={"Create a notification"}
>
<p>
Using your filters, we have found 10 leads that match your criteria.
Would you like to be notified when new leads are available?
</p>
<form className={"mt-4 flex flex-col gap-4"}>
<InputField label={"Name"} placeholder={"Enter a name"} />
<SelectField
placeholder={"Select an option"}
label={"Select how you would like to be notified"}
options={[]}
/>
<SelectField
label={"Select the frequency"}
placeholder={"Select an option"}
options={[]}
/>
<div className={"rounded-xl bg-gray-100 p-4"}>
<p className={"font-semibold"}>Your selected filters:</p>
<ul className={"ml-4 list-inside list-disc"}>
<li>
<strong>Mortgage Value:</strong> Below £100k
</li>
<li>
<strong>Loan to Value (LTV):</strong> Below 50%
</li>
<li>
<strong>Mortgage Purpose:</strong> Remortgage
</li>
<li>
<strong>Location:</strong> London
</li>
<li>
<strong>Credit Status:</strong> Good
</li>
<li>
<strong>Deal Stage:</strong> Early
</li>
</ul>
</div>
<div className={"flex items-center justify-end gap-4"}>
<button
onClick={(e) => {
e.preventDefault();
setOpenCreateNotification(false);
}}
className={
"rounded-xl bg-gray-100 px-4 py-2 font-semibold text-deep-sapphire-1000 transition-all hover:bg-gray-300"
}
>
Cancel
</button>
<button
onClick={(e) => {
e.preventDefault();
setOpenCreateNotification(false);
}}
className={
"rounded-xl bg-niagara-500 px-4 py-2 font-semibold text-white transition-all hover:bg-niagara-600"
}
>
Create
</button>
</div>
</form>
</Modal>
<FilterModal open={openFilters} onClose={() => {
setOpenFilters(false)
}}/>
</Card>
);
}
表.tsx
import React, { Fragment, useEffect } from "react";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getExpandedRowModel,
PaginationState,
Row,
useReactTable,
} from "@tanstack/react-table";
import { ILead, IMortgageLead } from "@/interfaces/ILead";
import { FaCaretLeft, FaCaretRight, FaCaretUp } from "react-icons/fa";
import cnMerge from "@/utils/cnMerge";
import { FaAnglesLeft, FaAnglesRight } from "react-icons/fa6";
import { IPaginatedList } from "@/interfaces/IPaginatedList";
import { UseTRPCQueryResult } from "@trpc/react-query/shared";
import { trpc } from "@/app/api/_trpc/client";
type TableProps<TData> = {
data: IPaginatedList<TData> | null;
columns: ColumnDef<TData>[];
renderSubComponent: (props: { row: Row<TData> }) => React.ReactElement;
getRowCanExpand: (row: Row<TData>) => boolean;
onPaginationChange?: (pagination: PaginationState) => void;
query: UseTRPCQueryResult<any, any>;
pagination: PaginationState
};
function Table({
data,
columns,
renderSubComponent,
getRowCanExpand,
onPaginationChange: _onPaginationChange,
query,
pagination: paginationState
}: TableProps<IMortgageLead>): JSX.Element {
const [{ pageIndex, pageSize }, setPagination] =
React.useState<PaginationState>({
pageIndex: paginationState.pageIndex,
pageSize: paginationState.pageSize,
});
useEffect(() => {
_onPaginationChange?.({ pageIndex, pageSize });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageIndex, pageSize]);
const pagination = React.useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize],
);
const table = useReactTable<IMortgageLead>({
data: data?.results ?? [],
columns,
getRowCanExpand,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
pageCount: Math.ceil((data?.count ?? 0) / pageSize),
state: {
pagination,
},
onPaginationChange: setPagination,
manualPagination: true,
debugTable: process.env.NODE_ENV === 'development',
});
const { isLoading, error, isError } = query;
if (isLoading) {
return (
<div className={"flex h-96 items-center justify-center"}>
<p>Loading...</p>
</div>
);
}
if (isError) {
return (
<div className={"flex h-96 items-center justify-center"}>
<p>Error: {error?.message}</p>
</div>
);
}
return (
<>
<div className={"overflow-x-auto"}>
<table className={"w-full text-xs"}>
<thead className={"bg-gray-100"}>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<th
key={header.id}
colSpan={header.colSpan}
className={
"h-11 whitespace-nowrap border-b border-r px-3 py-2 text-left"
}
>
{header.isPlaceholder ? null : (
<div>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</div>
)}
</th>
);
})}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => {
return (
<Fragment key={row.id}>
<tr>
{/* first row is a normal row */}
{row.getVisibleCells().map((cell) => {
return (
<td
key={cell.id}
className={cnMerge("h-11 border-b border-r px-2", {
"m-0 w-11 p-0": cell.column.id === "expander",
})}
>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</td>
);
})}
</tr>
{row.getIsExpanded() && (
<tr>
{/* 2nd row is a custom 1 cell row */}
<td colSpan={row.getVisibleCells().length}>
{renderSubComponent({ row })}
</td>
</tr>
)}
</Fragment>
);
})}
</tbody>
</table>
</div>
<div
className={
"flex w-full items-center justify-end gap-4 px-4 py-2 text-sm"
}
>
<div className={"flex gap-2"}>
<p>Rows per page:</p>
<select
className={"m-0 border-0 bg-none p-0"}
name="pageSize"
onChange={(e) => {
table.setPageSize(Number(e.target.value));
}}
>
{[15, 25, 50, 100].map((pageSize) => (
<option key={pageSize} value={pageSize}>
{pageSize}
</option>
))}
</select>
</div>
<div>
<p>
{table.getState().pagination.pageIndex} of{" "}
{table.getPageCount()}
</p>
</div>
<div className={"flex gap-2"}>
<button
className={
"flex h-12 w-12 items-center justify-center rounded-3xl bg-gray-100 text-lg text-deep-sapphire-1000 transition-all hover:bg-gray-300 disabled:cursor-not-allowed"
}
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<FaAnglesLeft />
</button>
<button
className={
"flex h-12 w-12 items-center justify-center rounded-3xl bg-gray-100 text-lg text-deep-sapphire-1000 transition-all hover:bg-gray-300 disabled:cursor-not-allowed"
}
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<FaCaretLeft />
</button>
<button
className={
"flex h-12 w-12 items-center justify-center rounded-3xl bg-gray-100 text-lg text-deep-sapphire-1000 transition-all hover:bg-gray-300 disabled:cursor-not-allowed"
}
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<FaCaretRight />
</button>
<button
className={
"flex h-12 w-12 items-center justify-center rounded-3xl bg-gray-100 text-lg text-deep-sapphire-1000 transition-all hover:bg-gray-300 disabled:cursor-not-allowed"
}
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<FaAnglesRight />
</button>
</div>
</div>
</>
);
}
export function ExpanderIcon({
open = false,
classNames = {},
}: {
open: boolean;
classNames?: { [key: string]: boolean };
}): JSX.Element {
return (
<span
className={cnMerge(
"flex h-11 w-11 items-center justify-center bg-emerald-500 text-2xl text-white h-full",
{
"bg-emerald-600": open,
"bg-emerald-500": !open,
...classNames,
},
)}
>
<FaCaretUp
className={cnMerge("origin-center transform transition-all", {
"rotate-180": open,
"rotate-90": !open,
})}
/>
</span>
);
}
export default Table;
这些是这些页面上使用的 3 个组件,具有任何形式的逻辑来导致这种情况。
有没有人对我如何解决这个问题有任何想法?
我目前使用 TRPC 将数据从后端获取到前端,并使用 TanStack React Table 来显示表
提前致谢
在尝试解决这个问题时,我几乎尝试了所有方法,除非物理重写代码。
我担心这可能是由于我将查询作为道具传递到表,以允许我在某些特定的表操作上重新获取
答: 暂无答案
评论
const columns = React.useMemo(() => [
if (......) return
// eslint-disable-next-line react-hooks/rules-of-hooks