错误:呈现的钩子比预期的要少。这可能是由意外的提前退货声明引起的。反应查询

Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement. React-Query

提问人:Kieran Corkin 提问时间:11/15/2023 更新时间:11/15/2023 访问量:22

问:

因此,我目前正在尝试使用来自后端的一些数据将表格呈现到页面上,以显示某种市场,但我似乎遇到了这个非常令人沮丧的错误,信息不多。我也在努力诊断这个问题。这似乎也不一致。它大部分时间都显示,但有时似乎不显示。请参阅下面的错误:

enter image description here

页面.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 来显示表

提前致谢

在尝试解决这个问题时,我几乎尝试了所有方法,除非物理重写代码。

我担心这可能是由于我将查询作为道具传递到表,以允许我在某些特定的表操作上重新获取

javascript reactjs next.js react-table

评论

2赞 Keith 11/15/2023
你有 after -> 你不能那样做。所有钩子在每次重新渲染时都必须以相同的顺序运行。const columns = React.useMemo(() => [if (......) return
1赞 James 11/15/2023
您添加了这样 eslint 就不会抱怨您现在遇到的问题。// eslint-disable-next-line react-hooks/rules-of-hooks

答: 暂无答案