Utils
use-remote-pagination
Debounced keyword search plus append-based pagination for remote data sources
Installation
$npx litefy@latest add use-remote-pagination
$pnpm dlx litefy@latest add use-remote-pagination
$yarn dlx litefy@latest add use-remote-pagination
$bun --bun litefy@latest add use-remote-pagination
Usage
Pair a debounced search with loadMore to query remote sources. The same fetcher handles both; a single in-flight request guard prevents duplicated fetches.
- No data
"use client";import * as React from "react";import { useRemotePagination } from "@/ui";const fetcher = ({ page, size, keyword }: { page: number; size: number; keyword: string }) => new Promise<{ list: string[]; total: number }>((resolve) => { setTimeout(() => { const start = (page - 1) * size + 1; const list = Array.from( { length: Math.min(size, 40 - start + 1) }, (_, i) => `${keyword || "item"} #${start + i}`, ); resolve({ list, total: 40 }); }, 600); });export default function UseRemotePaginationBasicDemo() { const [keyword, setKeyword] = React.useState(""); const remote = useRemotePagination({ fetcher }); const handleScroll = (e: React.UIEvent<HTMLDivElement>) => { if (!remote.hasMore || remote.loading) return; const el = e.currentTarget; if (el.scrollHeight - el.scrollTop - el.clientHeight < 40) { remote.loadMore(); } }; return ( <div className="flex w-full max-w-sm flex-col gap-3 py-4"> <input value={keyword} onChange={(e) => { setKeyword(e.target.value); remote.search(e.target.value); }} placeholder="Search…" className="h-9 rounded-md border px-3 text-sm outline-none focus:border-primary" /> {remote.isSearching ? ( <p className="py-4 text-center text-sm text-muted-foreground">Searching…</p> ) : ( <div onScroll={handleScroll} className="h-64 overflow-y-auto rounded-md border" > <ul className="flex flex-col divide-y"> {remote.data.map((item) => ( <li key={item} className="px-3 py-2 text-sm"> {item} </li> ))} {remote.data.length === 0 && ( <li className="px-3 py-4 text-center text-sm text-muted-foreground"> No data </li> )} {remote.loading && ( <li className="px-3 py-2 text-center text-sm text-muted-foreground"> Loading… </li> )} </ul> </div> )} </div> );}API Reference
useRemotePagination
function useRemotePagination(opts: UseRemotePaginationOptions): UseRemotePaginationResultUseRemotePaginationOptions
| Property | Type | Default | Description |
|---|---|---|---|
fetcher | RemotePaginationFn | - | Async function receiving { page, size, keyword } and returning { list, total } |
debounceMs | number | 300 | Debounce delay for search |
pageSize | number | 20 | Items requested per page |
RemotePaginationFn
type RemotePaginationFn = (params: {
page: number;
size: number;
keyword: string;
}) => Promise<{ list: string[]; total: number }>;UseRemotePaginationResult
| Property | Type | Description |
|---|---|---|
data | string[] | Accumulated list; replaced on search, appended on loadMore |
loading | boolean | Whether a request is in flight |
isSearching | boolean | Whether a fresh (page 1) request is in flight, for showing search placeholders |
hasMore | boolean | Whether more pages remain, derived from list.length < total |
search | (keyword: string) => void | Debounced keyword search; resets to page 1 and replaces data |
loadMore | () => void | Fetch the next page with the current keyword and append |
reset | () => void | Clear data and restore the initial state |
Notes:
- The fetcher response shape is fixed to
{ list, total }; map your API response to this shape before returning. listitems are typed asstring[]in this version; key them uniquely downstream.- Only one request runs at a time; concurrent triggers are dropped rather than queued.