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"
/>
{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.