Search Values Mode

Pagination is a good fit for writing the current position to the URL. This example uses page and pageSize to control a local list. Invalid values fall back to defaults, and changing pageSize resets page to 1.

Docs list

Showing 1-5 of 37

Page size
#1

Decurl example 1

Search params item 1

#2

Decurl example 2

Search params item 2

#3

Decurl example 3

Search params item 3

#4

Decurl example 4

Search params item 4

#5

Decurl example 5

Search params item 5

Page 1 of 8

location.search(empty)
page: 1pageSize: 5
import { useSearchValues } from '@guanriyue/decurl';
import { defineFields, field } from '@guanriyue/decurl/codec';
import {
  elementOf,
  min,
  pipe,
  shape,
  toNumber,
  trim,
} from '@guanriyue/decurl/decode';
import { ChevronLeft, ChevronRight } from 'lucide-react';
import { useLocation } from 'react-router';

import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
  Pagination,
  PaginationContent,
  PaginationEllipsis,
  PaginationItem,
} from '@/components/ui/pagination';

const pageSizeValues = [5, 10, 20];

const fields = defineFields({
  page: field({
    name: 'example_page',
    decode: pipe(trim, shape.integer, toNumber, min(1)),
    defaultValue: 1,
  }),
  pageSize: field({
    name: 'example_page_size',
    decode: pipe(trim, shape.integer, toNumber, elementOf(pageSizeValues)),
    defaultValue: 5,
  }),
});

const items = Array.from({ length: 37 }, (_, index) => {
  const id = index + 1;

  return {
    id,
    title: `Decurl example ${id}`,
    description: `Search params item ${id}`,
  };
});

const toSearchText = (search: string): string => {
  return search.length > 0 ? search : '(empty)';
};

const getVisiblePages = (page: number, pageCount: number): number[] => {
  const start = Math.max(1, page - 1);
  const end = Math.min(pageCount, page + 1);

  return Array.from({ length: end - start + 1 }, (_, index) => start + index);
};

const PaginationDemo = () => {
  const location = useLocation();
  const [values, setValues] = useSearchValues(fields);
  const pageCount = Math.max(1, Math.ceil(items.length / values.pageSize));
  const page = Math.min(values.page, pageCount);
  const startIndex = (page - 1) * values.pageSize;
  const currentItems = items.slice(startIndex, startIndex + values.pageSize);
  const visiblePages = getVisiblePages(page, pageCount);

  const updatePage = (nextPage: number) => {
    setValues({
      page: Math.min(Math.max(nextPage, 1), pageCount),
    });
  };

  return (
    <section className="mx-auto w-full max-w-5xl space-y-6 p-4">
      <div className="space-y-4">
        <div className="flex flex-wrap items-center justify-between gap-3">
          <div>
            <h2 className="text-base font-semibold">Docs list</h2>
            <p className="text-sm text-muted-foreground">
              Showing {startIndex + 1}-{startIndex + currentItems.length} of{' '}
              {items.length}
            </p>
          </div>
          <div className="flex flex-wrap items-center gap-2">
            <span className="text-sm text-muted-foreground">Page size</span>
            {pageSizeValues.map((pageSize) => (
              <Button
                key={pageSize}
                type="button"
                size="sm"
                variant={values.pageSize === pageSize ? 'default' : 'outline'}
                onClick={() => {
                  setValues({
                    page: 1,
                    pageSize,
                  });
                }}
              >
                {pageSize}
              </Button>
            ))}
          </div>
        </div>

        <div className="grid gap-2 sm:grid-cols-2">
          {currentItems.map((item) => (
            <article key={item.id} className="rounded-md bg-muted/40 p-3">
              <div className="flex items-center gap-2">
                <Badge variant="outline">#{item.id}</Badge>
                <h3 className="text-sm font-medium">{item.title}</h3>
              </div>
              <p className="mt-1 text-sm text-muted-foreground">
                {item.description}
              </p>
            </article>
          ))}
        </div>

        <div className="space-y-3">
          <Pagination>
            <PaginationContent>
              <PaginationItem>
                <Button
                  type="button"
                  variant="ghost"
                  disabled={page <= 1}
                  onClick={() => {
                    updatePage(page - 1);
                  }}
                >
                  <ChevronLeft />
                  Previous
                </Button>
              </PaginationItem>

              {visiblePages[0] > 1 && (
                <>
                  <PaginationItem>
                    <Button
                      type="button"
                      size="icon"
                      variant="ghost"
                      onClick={() => {
                        updatePage(1);
                      }}
                    >
                      1
                    </Button>
                  </PaginationItem>
                  {visiblePages[0] > 2 && (
                    <PaginationItem>
                      <PaginationEllipsis />
                    </PaginationItem>
                  )}
                </>
              )}

              {visiblePages.map((visiblePage) => (
                <PaginationItem key={visiblePage}>
                  <Button
                    type="button"
                    size="icon"
                    variant={visiblePage === page ? 'outline' : 'ghost'}
                    aria-current={visiblePage === page ? 'page' : undefined}
                    onClick={() => {
                      updatePage(visiblePage);
                    }}
                  >
                    {visiblePage}
                  </Button>
                </PaginationItem>
              ))}

              {visiblePages[visiblePages.length - 1] < pageCount && (
                <>
                  {visiblePages[visiblePages.length - 1] < pageCount - 1 && (
                    <PaginationItem>
                      <PaginationEllipsis />
                    </PaginationItem>
                  )}
                  <PaginationItem>
                    <Button
                      type="button"
                      size="icon"
                      variant="ghost"
                      onClick={() => {
                        updatePage(pageCount);
                      }}
                    >
                      {pageCount}
                    </Button>
                  </PaginationItem>
                </>
              )}

              <PaginationItem>
                <Button
                  type="button"
                  variant="ghost"
                  disabled={page >= pageCount}
                  onClick={() => {
                    updatePage(page + 1);
                  }}
                >
                  Next
                  <ChevronRight />
                </Button>
              </PaginationItem>
            </PaginationContent>
          </Pagination>

          <p className="text-center text-sm text-muted-foreground">
            Page {page} of {pageCount}
          </p>
        </div>
      </div>

      <div className="space-y-4 rounded-md bg-muted/40 p-4 text-sm">
        <div className="space-y-2">
          <span className="font-medium">location.search</span>
          <code className="block break-all rounded-md bg-background/70 px-2.5 py-2 font-mono text-xs">
            {toSearchText(location.search)}
          </code>
        </div>
        <div className="flex flex-wrap gap-2">
          <Badge variant="secondary">page: {values.page}</Badge>
          <Badge variant="secondary">pageSize: {values.pageSize}</Badge>
        </div>
      </div>
    </section>
  );
};

export default PaginationDemo;