nextui icon indicating copy to clipboard operation
nextui copied to clipboard

[BUG] - Inconsistent URL parameters when using useSearchParams with pagination

Open Subham-Maity opened this issue 1 year ago • 2 comments

NextUI Version

latest

Describe the bug

Issue:

When using useSearchParams from next/navigation to implement pagination, the URL parameters are not consistently updated.

Steps to reproduce:

  1. Implement pagination using useSearchParams to get the current page number from the URL's query parameters.
  2. Navigate to a new page

Your Example Website or App

No response

Steps to Reproduce the Bug or Issue

Just copy the code and try this pagination component.

Expected behavior

Expected behavior:

The URL should always include both the page and sort parameters, regardless of whether you're navigating to a new page or changing the sort option.

expected

?_sort=price&_order=asc&_page=3
?_sort=price&_order=asc&_page=4
?_sort=price&_order=asc&_page=5

but got

?_sort=price&_order=asc&_page=1
?__page=2
?_sort=price&_order=asc&_page=3

Actual behavior: The URL does not consistently include both the page and sort parameters. When navigating to a new page, the sort parameters are sometimes missing from the URL, and vice versa.

Screenshots or Videos

Ok first see the difference

This is the video where I use nextui

https://github.com/nextui-org/nextui/assets/97989643/82803b27-789c-4df7-821e-248ad3a2a2f1

Actual Code with nextui

import { ITEMS_PER_PAGE } from "@/constant/constants";
import React from "react";
import { Pagination } from "@nextui-org/react";
import { useRouter, useSearchParams } from "next/navigation";
import { useAppSelector } from "@/store/redux/useSelector";
import { selectTotalItems } from "@/lib/features/product/product-pc-slice";
import PaginationSkeleton from "@/loader/skeleton/product-t1/pagination-skeleton";
import { defaultUrlPagination } from "@/links/product-list";

export function PaginationPage() {
  const searchParams = useSearchParams();
  const page = parseInt(searchParams.get("_page") || "1");
  const totalItems = useAppSelector(selectTotalItems);
  const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);

  const router = useRouter();
  const handlePageChange = (newPage: number) => {
    const newSearchParams = new URLSearchParams(searchParams.toString());
    newSearchParams.set("_page", newPage.toString());
    router.push(`${defaultUrlPagination}${newSearchParams.toString()}`);
  };

  if (!totalItems) {
    return <PaginationSkeleton />;
  }

  return (
    <div className="flex items-center justify-between border-t border-gray-200 px-4 py-3 sm:px-6">
      <div className="hidden lg:block">
        <p className="text-sm text-gray-700 dark:text-gray-200">
          Showing
          <span className="font-medium">
            {(page - 1) * ITEMS_PER_PAGE + 1}
          </span>{" "}
          to
          <span className="font-medium">
            {page * ITEMS_PER_PAGE > totalItems
              ? totalItems
              : page * ITEMS_PER_PAGE}
          </span>
          of <span className="font-medium">{totalItems}</span> results
        </p>
      </div>
      <Pagination
        initialPage={1}
        showControls
        showShadow
        boundaries={2}
        total={totalPages || 1}
        page={page || 1}
        onChange={handlePageChange}
        color="primary"
        variant="bordered"
      />
    </div>
  );
}

Upon implementing the same approach with Shadcn, I observed that it works as expected.

https://github.com/nextui-org/nextui/assets/97989643/a9c5223e-51f2-48a0-8672-ab858a95d211

import React from "react";
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
  PaginationLink,
} from "@/components/ui/shadcn/pagination";
import { ITEMS_PER_PAGE } from "@/constant/constants";
import { Button } from "@/components/ui/shadcn/button";
import { useAppSelector } from "@/store/redux/useSelector";
import { selectTotalItems } from "@/lib/features/product/product-pc-slice";
import { useRouter, useSearchParams } from "next/navigation";
import PaginationSkeleton from "@/loader/skeleton/product-t1/pagination-skeleton";
import { defaultUrlPagination } from "@/links/product-list";

export function PaginationPage() {
  const searchParams = useSearchParams();
  const page = parseInt(searchParams.get("_page") || "1");
  const totalItems = useAppSelector(selectTotalItems);
  const totalPages = Math.ceil(totalItems / ITEMS_PER_PAGE);

  const router = useRouter();
  const handlePageChange = (newPage: number) => {
    const newSearchParams = new URLSearchParams(searchParams.toString());
    newSearchParams.set("_page", newPage.toString());
    router.push(`${defaultUrlPagination}${newSearchParams.toString()}`);
  };

  if (!totalItems) {
    return <PaginationSkeleton />;
  }

  return (
    <div className="flex flex-col sm:flex-row justify-between items-center">
      <p className="text-sm text-gray-700 dark:text-gray-200 mb-2 sm:mb-0">
        Showing
        <span className="font-medium">{(page - 1) * ITEMS_PER_PAGE + 1}</span>
        to
        <span className="font-medium">
          {page * ITEMS_PER_PAGE > totalItems
            ? totalItems
            : page * ITEMS_PER_PAGE}
        </span>{" "}
        of <span className="font-medium">{totalItems}</span> results
      </p>
      <Pagination>
        <PaginationContent>
          <PaginationItem>
            <Button
              variant="secondary"
              onClick={() => handlePageChange(page > 1 ? page - 1 : page)}
            >
              Previous
            </Button>
          </PaginationItem>
          {Array.from({ length: totalPages }).map((_, index) => (
            <PaginationItem key={index}>
              <PaginationLink
                href="#"
                isActive={index + 1 === page}
                onClick={() => handlePageChange(index + 1)}
              >
                {index + 1}
              </PaginationLink>
            </PaginationItem>
          ))}
          {page < totalPages && <PaginationEllipsis />}
          <PaginationItem>
            <Button
              variant="secondary"
              onClick={() =>
                handlePageChange(page < totalPages ? page + 1 : page)
              }
            >
              Next
            </Button>
          </PaginationItem>
        </PaginationContent>
      </Pagination>
    </div>
  );
}

Operating System Version

windows, mac

Browser

Chrome

Subham-Maity avatar Feb 29 '24 06:02 Subham-Maity

Bump - I have ran into the same issue using Remix. It took 2 days, but eventually tracked it down to this Pagination component. Not sure how it interferes with useSearchParams exactly, but, I wonder if it has something to do with some internal caching of the onChange event?

jesuzon avatar May 01 '24 17:05 jesuzon

Also just run into that problem. Took me like 3 hours to realise that the problemis related to this component. The problem seems to be somehow related to the onChange.

<Pagination
  isCompact
  showControls
  showShadow
  total={loaderData.meta.totalPages}
  onChange={(page) => setPage(page)}
/>

When I use external buttons with the same function, everything works.

 <Button onClick={() => setPage(loaderData.meta.currentPage - 1)}>prev</Button>

EDIT:

I was able to workaround this problem using a useState.

 const [pageState, setPageState] = useState<{
    prevPage: number;
    currentPage: number;
  }>({
    prevPage: loaderData.meta.currentPage,
    currentPage: loaderData.meta.currentPage,
  });

  useEffect(() => {
    if (pageState.prevPage !== pageState.currentPage) {
      setPageState({
        prevPage: pageState.currentPage,
        currentPage: pageState.currentPage,
      });
      setPage(pageState.currentPage);
    }
  }, [pageState, setPage]);

<Pagination
   isCompact
   showControls
   showShadow
   initialPage={loaderData.meta.currentPage}
   page={pageState.currentPage}
   total={loaderData.meta.totalPages}
   onChange={(page) =>
      setPageState({
      prevPage: pageState.currentPage,
      currentPage: page,
    })
    }
/>

sharenz avatar May 26 '24 13:05 sharenz