> A powerful React hook for managing client-side paginated data with filters, automatic refetching, and built-in loading states
npm install @mutasimalmu/use-paginated-list-query> A powerful React hook for managing client-side paginated data with filters, automatic refetching, and built-in loading states
- 🚀 Client-Side Pagination - Efficiently handle large datasets with automatic page management
- 🔍 Built-in Filtering - Seamlessly filter data with automatic refetch on filter changes
- ⚡ Debounced Requests - Prevent excessive API calls with configurable debounce timing (default: 500ms)
- 🎯 TypeScript Support - Full type inference for your fetch methods and data structures
- 🔄 Auto-Refetch - Automatically refetches data when page or filters change
- 💪 Request Abortion - Automatic cleanup and abortion of pending requests
- 🎨 Loading & Error States - Built-in loading and error state management
- 🛠️ Flexible API - Works with any paginated API endpoint
- 🔌 Framework Agnostic - Works with any React-based application
``bash`
npm install @mutasimalmu/use-paginated-list-query
or
`bash`
yarn add @mutasimalmu/use-paginated-list-query
`typescript
import usePaginatedList, {
PaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';
import { Table } from 'antd';
import type { ColumnsType } from 'antd/es/table';
interface User {
id: string;
name: string;
email: string;
role: string;
}
async function fetchUsers(
args: PaginatedListRequest
): Promise
const response = await fetch(
/api/users?page=${args.page}&page_size=${args.page_size},
{ signal: args.signal }
);
return response.json();
}
function UsersPage() {
const {
data,
isLoading,
page,
pageSize,
totalCount,
changePage,
} = usePaginatedList({
fetchMethod: fetchUsers,
pageSize: 10,
});
const columns: ColumnsType
{
title: 'Name',
dataIndex: 'name',
key: 'name',
},
{
title: 'Email',
dataIndex: 'email',
key: 'email',
},
{
title: 'Role',
dataIndex: 'role',
key: 'role',
},
];
return (
$3
`typescript
import usePaginatedList, {
PaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { Button } from '@/components/ui/button';interface Product {
id: string;
name: string;
price: number;
category: string;
}
async function fetchProducts(
args: PaginatedListRequest
): Promise> {
const response = await fetch(
/api/products?page=${args.page}&page_size=${args.page_size},
{ signal: args.signal }
);
return response.json();
}function ProductsPage() {
const {
data: products,
isLoading,
page,
pageSize,
totalCount,
changePage,
} = usePaginatedList({
fetchMethod: fetchProducts,
pageSize: 20,
});
const totalPages = Math.ceil(totalCount / pageSize);
if (isLoading) {
return
Loading...;
} return (
Name
Price
Category
{products.map((product) => (
{product.name}
${product.price}
{product.category}
))}
variant="outline"
onClick={() => changePage(page - 1)}
disabled={page === 1}
>
Previous
Page {page} of {totalPages}
variant="outline"
onClick={() => changePage(page + 1)}
disabled={page === totalPages}
>
Next
);
}
`📚 API Reference
$3
`typescript
function usePaginatedList(props: Props): PaginatedListResult
`$3
| Property | Type | Required | Default | Description |
|----------|------|----------|---------|-------------|
|
fetchMethod | FetchMethod | ✅ Yes | - | Your API fetch function that returns paginated data |
| pageSize | number | ❌ No | 10 | Number of items per page |
| debounceTime | number | ❌ No | 500 | Debounce time in milliseconds for filter changes |
| initialState | Partial | ❌ No | {} | Initial state for filters, data, page, etc. |
| disabled | boolean | ❌ No | false | When true, prevents automatic data fetching |
| pathParams | object | ❌ Conditional | - | Path parameters required by your fetch method (if needed) |Note: The hook initializes with
filters: { searchValue: "" } by default. You can override this using the initialState prop.$3
| Property | Type | Description |
|----------|------|-------------|
|
data | T[] | Array of fetched items |
| isLoading | boolean | Loading state indicator |
| page | number | Current page number (1-indexed) |
| pageSize | number | Number of items per page |
| totalCount | number | Total number of items across all pages |
| filters | F | Current filter values |
| onFiltersChange | (filters: F) => void | Function to update filters |
| changePage | (page: number) => void | Function to change the current page |
| refetchData | () => Promise | Manually trigger a data refetch |
| resetFilters | () => void | Reset all filters to empty state |
| error | unknown | Error object if fetch failed |
| abort | () => void | Manually abort the current request |🎯 Fetch Method Requirements
The package exports utility types and interfaces to help you build type-safe fetch methods. Import and use these types for proper TypeScript inference:
`typescript
import {
PaginatedListRequest,
FilterablePaginatedListRequest,
PathablePaginatedListRequest,
FilterablePathablePaginatedListRequest,
PaginatedListResponse,
// Or use the pre-built method types:
FetchPaginatedListMethod,
FetchFilterablePaginatedListMethod,
FetchPathablePaginatedListMethod,
FetchFilterablePathablePaginatedListMethod,
} from '@mutasimalmu/use-paginated-list-query';
`$3
Using Request/Response interfaces:
`typescript
async function fetchUsers(
args: PaginatedListRequest
): Promise> {
const response = await fetch(
/api/users?page=${args.page}&page_size=${args.page_size},
{ signal: args.signal }
);
return response.json();
}
`Or using the pre-built method type:
`typescript
const fetchUsers: FetchPaginatedListMethod = async (args) => {
const response = await fetch(
/api/users?page=${args.page}&page_size=${args.page_size},
{ signal: args.signal }
);
return response.json();
};
`$3
Using Request/Response interfaces:
`typescript
interface ProductFilters {
searchValue?: string;
category?: string;
}async function fetchProducts(
args: FilterablePaginatedListRequest
): Promise> {
const params = new URLSearchParams({
page: args.page.toString(),
page_size: args.page_size.toString(),
...args.filters,
});
const response = await fetch(
/api/products?${params}, { signal: args.signal });
return response.json();
}
`Or using the pre-built method type:
`typescript
const fetchProducts: FetchFilterablePaginatedListMethod = async (args) => {
// Implementation
};
`$3
Using Request/Response interfaces:
`typescript
interface CategoryPathParams {
categoryId: string;
}async function fetchCategoryProducts(
args: PathablePaginatedListRequest
): Promise> {
const response = await fetch(
/api/categories/${args.pathParams.categoryId}/products?page=${args.page}&page_size=${args.page_size},
{ signal: args.signal }
);
return response.json();
}
`Or using the pre-built method type:
`typescript
const fetchCategoryProducts: FetchPathablePaginatedListMethod = async (args) => {
// Implementation
};
`$3
Using Request/Response interfaces:
`typescript
interface ProductFilters {
searchValue?: string;
minPrice?: number;
}interface CategoryPathParams {
categoryId: string;
}
async function fetchCategoryProducts(
args: FilterablePathablePaginatedListRequest
): Promise> {
const params = new URLSearchParams({
page: args.page.toString(),
page_size: args.page_size.toString(),
...args.filters,
});
const response = await fetch(
/api/categories/${args.pathParams.categoryId}/products?${params},
{ signal: args.signal }
);
return response.json();
}
`Or using the pre-built method type:
`typescript
const fetchCategoryProducts: FetchFilterablePathablePaginatedListMethod<
Product,
ProductFilters,
CategoryPathParams
> = async (args) => {
// Implementation
};
`✨ Benefits of using these types:
- ✅ Full TypeScript inference throughout your application
- ✅ Compile-time type checking for request/response structures
- ✅ Better IDE autocomplete and IntelliSense
- ✅ Prevents common typing mistakes
Note: The hook automatically detects which parameters your fetch method expects and passes them accordingly.
$3
`typescript
import {
FilterablePaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';interface ProductFilters {
searchValue?: string;
category?: string;
minPrice?: number;
maxPrice?: number;
}
interface Product {
id: string;
name: string;
price: number;
category: string;
stock: number;
}
async function fetchProducts(
args: FilterablePaginatedListRequest
): Promise> {
const params = new URLSearchParams({
page: args.page.toString(),
page_size: args.page_size.toString(),
...args.filters,
});
const response = await fetch(
/api/products?${params}, {
signal: args.signal,
}); if (!response.ok) {
throw new Error('Failed to fetch products');
}
return response.json();
}
`💡 Advanced Usage Examples
$3
`typescript
import usePaginatedList, {
FilterablePaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';
import { Table, Input, Select, Button, Space } from 'antd';interface Product {
id: string;
name: string;
price: number;
category: string;
}
interface ProductFilters {
searchValue?: string;
category?: string;
}
async function fetchProducts(
args: FilterablePaginatedListRequest
): Promise> {
const params = new URLSearchParams({
page: args.page.toString(),
page_size: args.page_size.toString(),
...args.filters,
});
const response = await fetch(
/api/products?${params}, { signal: args.signal });
return response.json();
}function ProductsPage() {
const {
data,
isLoading,
page,
pageSize,
totalCount,
filters,
onFiltersChange,
changePage,
resetFilters,
} = usePaginatedList({
fetchMethod: fetchProducts,
pageSize: 25,
initialState: {
filters: {
category: 'electronics',
},
},
});
const columns = [
{ title: 'Name', dataIndex: 'name', key: 'name' },
{ title: 'Price', dataIndex: 'price', key: 'price', render: (price) =>
$${price} },
{ title: 'Category', dataIndex: 'category', key: 'category' },
]; return (
placeholder="Search products..."
value={filters.searchValue}
onChange={(e) => onFiltersChange({ searchValue: e.target.value })}
style={{ width: 200 }}
/>
placeholder="Select category"
value={filters.category}
onChange={(value) => onFiltersChange({ category: value })}
style={{ width: 150 }}
>
Electronics
Clothing
Books
columns={columns}
dataSource={data}
loading={isLoading}
rowKey="id"
pagination={{
current: page,
pageSize: pageSize,
total: totalCount,
onChange: changePage,
}}
/>
);
}
`$3
`typescript
import usePaginatedList, {
FilterablePaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';interface Article {
id: string;
title: string;
excerpt: string;
status: string;
}
interface ArticleFilters {
searchValue?: string;
status?: string;
}
async function fetchArticles(
args: FilterablePaginatedListRequest
): Promise> {
const params = new URLSearchParams({
page: args.page.toString(),
page_size: args.page_size.toString(),
...args.filters,
});
const response = await fetch(
/api/articles?${params}, { signal: args.signal });
return response.json();
}function ArticlesPage() {
const {
data: articles,
isLoading,
page,
pageSize,
totalCount,
filters,
onFiltersChange,
changePage,
resetFilters,
} = usePaginatedList({
fetchMethod: fetchArticles,
pageSize: 12,
});
return (
placeholder="Search articles..."
value={filters.searchValue || ''}
onChange={(e) => onFiltersChange({ searchValue: e.target.value })}
className="max-w-sm"
/>
value={filters.status}
onValueChange={(value) => onFiltersChange({ status: value })}
>
Published
Draft
Archived
{isLoading ? (
Loading...
) : (
{articles.map((article) => (
{article.title}
{article.excerpt}
))}
)}
variant="outline"
onClick={() => changePage(page - 1)}
disabled={page === 1}
>
Previous
Page {page} of {Math.ceil(totalCount / pageSize)}
variant="outline"
onClick={() => changePage(page + 1)}
disabled={page === Math.ceil(totalCount / pageSize)}
>
Next
);
}
`$3
`typescript
import usePaginatedList, {
PathablePaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';
import { Table, Tag } from 'antd';interface CategoryPathParams {
categoryId: string;
}
interface Product {
id: string;
name: string;
price: number;
inStock: boolean;
}
async function fetchProductsByCategory(
args: PathablePaginatedListRequest
): Promise> {
const response = await fetch(
/api/categories/${args.pathParams.categoryId}/products?page=${args.page}&page_size=${args.page_size},
{ signal: args.signal }
);
return response.json();
}function CategoryProductsPage({ categoryId }: { categoryId: string }) {
const {
data: products,
isLoading,
page,
pageSize,
totalCount,
changePage,
} = usePaginatedList({
fetchMethod: fetchProductsByCategory,
pathParams: { categoryId },
pageSize: 20,
});
const columns = [
{ title: 'Product Name', dataIndex: 'name', key: 'name' },
{
title: 'Price',
dataIndex: 'price',
key: 'price',
render: (price) =>
$${price.toFixed(2)},
},
{
title: 'Status',
dataIndex: 'inStock',
key: 'inStock',
render: (inStock) => (
{inStock ? 'In Stock' : 'Out of Stock'}
),
},
]; return (
Products ({totalCount} total)
columns={columns}
dataSource={products}
loading={isLoading}
rowKey="id"
pagination={{
current: page,
pageSize: pageSize,
total: totalCount,
onChange: changePage,
}}
/>
);
}
`$3
`typescript
import usePaginatedList, {
FilterablePaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';
import { Input, List, Avatar } from 'antd';
import { SearchOutlined } from '@ant-design/icons';interface User {
id: string;
name: string;
email: string;
avatar: string;
}
interface UserFilters {
searchValue?: string;
}
async function fetchUsers(
args: FilterablePaginatedListRequest
): Promise> {
const params = new URLSearchParams({
page: args.page.toString(),
page_size: args.page_size.toString(),
...args.filters,
});
const response = await fetch(
/api/users?${params}, { signal: args.signal });
return response.json();
}function UserSearchPage() {
const {
data: users,
isLoading,
onFiltersChange,
} = usePaginatedList({
fetchMethod: fetchUsers,
debounceTime: 300, // Faster response for search (default is 500ms)
pageSize: 15,
});
return (
prefix={ }
placeholder="Search users..."
onChange={(e) => onFiltersChange({ searchValue: e.target.value })}
style={{ marginBottom: 16 }}
size="large"
/>
loading={isLoading}
dataSource={users}
renderItem={(user) => (
avatar={ }
title={user.name}
description={user.email}
/>
)}
/>
);
}
`$3
`typescript
import usePaginatedList, {
PaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';
import { Table, Button, message } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';interface Order {
id: string;
customerName: string;
total: number;
status: string;
}
async function fetchOrders(
args: PaginatedListRequest
): Promise> {
const response = await fetch(
/api/orders?page=${args.page}&page_size=${args.page_size},
{ signal: args.signal }
);
return response.json();
}function OrdersTable() {
const {
data: orders,
isLoading,
page,
pageSize,
totalCount,
changePage,
refetchData,
} = usePaginatedList({
fetchMethod: fetchOrders,
pageSize: 10,
});
const handleRefresh = async () => {
await refetchData();
message.success('Data refreshed successfully!');
};
const columns = [
{ title: 'Order ID', dataIndex: 'id', key: 'id' },
{ title: 'Customer', dataIndex: 'customerName', key: 'customerName' },
{ title: 'Total', dataIndex: 'total', key: 'total', render: (total) =>
$${total} },
{ title: 'Status', dataIndex: 'status', key: 'status' },
]; return (
icon={ }
onClick={handleRefresh}
loading={isLoading}
>
Refresh Data
columns={columns}
dataSource={orders}
loading={isLoading}
rowKey="id"
pagination={{
current: page,
pageSize: pageSize,
total: totalCount,
onChange: changePage,
}}
/>
);
}
`$3
`typescript
import usePaginatedList, {
PaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';
import { Table, Switch, Alert } from 'antd';
import { useState } from 'react';interface Product {
id: string;
name: string;
price: number;
}
async function fetchProducts(
args: PaginatedListRequest
): Promise> {
const response = await fetch(
/api/products?page=${args.page}&page_size=${args.page_size},
{ signal: args.signal }
);
return response.json();
}function ConditionalProductsTable() {
const [enabled, setEnabled] = useState(false);
const {
data: products,
isLoading,
page,
pageSize,
totalCount,
changePage,
} = usePaginatedList({
fetchMethod: fetchProducts,
disabled: !enabled, // Only fetch when enabled
pageSize: 10,
});
const columns = [
{ title: 'Product', dataIndex: 'name', key: 'name' },
{ title: 'Price', dataIndex: 'price', key: 'price' },
];
return (
checked={enabled}
onChange={setEnabled}
checkedChildren="Enabled"
unCheckedChildren="Disabled"
/>
Toggle data fetching
{!enabled ? (
message="Data fetching is disabled"
description="Enable the switch above to start fetching data."
type="info"
/>
) : (
columns={columns}
dataSource={products}
loading={isLoading}
rowKey="id"
pagination={{
current: page,
pageSize: pageSize,
total: totalCount,
onChange: changePage,
}}
/>
)}
);
}
`$3
`typescript
import usePaginatedList, {
FilterablePaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';
import { Table, Input, Select, Button, Space, Tag, message, Alert } from 'antd';
import { SearchOutlined, ReloadOutlined, ClearOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';interface User {
id: string;
name: string;
email: string;
role: string;
status: 'active' | 'inactive';
createdAt: string;
}
interface UserFilters {
searchValue?: string;
role?: string;
status?: string;
}
async function fetchUsers(
args: FilterablePaginatedListRequest
): Promise> {
const params = new URLSearchParams({
page: args.page.toString(),
page_size: args.page_size.toString(),
...(args.filters.searchValue && { search: args.filters.searchValue }),
...(args.filters.role && { role: args.filters.role }),
...(args.filters.status && { status: args.filters.status }),
});
const response = await fetch(
/api/users?${params}, { signal: args.signal });
return response.json();
}function UsersManagementPage() {
const {
data: users,
isLoading,
page,
pageSize,
totalCount,
filters,
onFiltersChange,
changePage,
refetchData,
resetFilters,
error,
} = usePaginatedList({
fetchMethod: fetchUsers,
pageSize: 20,
debounceTime: 400,
initialState: {
filters: {
status: 'active',
},
},
});
const handleRefresh = async () => {
await refetchData();
message.success('Data refreshed!');
};
const columns: ColumnsType = [
{
title: 'Name',
dataIndex: 'name',
key: 'name',
sorter: (a, b) => a.name.localeCompare(b.name),
},
{
title: 'Email',
dataIndex: 'email',
key: 'email',
},
{
title: 'Role',
dataIndex: 'role',
key: 'role',
render: (role) => {role} ,
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status) => (
{status.toUpperCase()}
),
},
{
title: 'Created',
dataIndex: 'createdAt',
key: 'createdAt',
render: (date) => new Date(date).toLocaleDateString(),
},
];
if (error) {
return (
message="Error Loading Users"
description="Failed to load users. Please try again."
type="error"
showIcon
action={
}
/>
);
}
return (
Users Management
prefix={ }
placeholder="Search users..."
value={filters.searchValue || ''}
onChange={(e) => onFiltersChange({ searchValue: e.target.value })}
style={{ width: 250 }}
allowClear
/>
placeholder="Filter by role"
value={filters.role}
onChange={(value) => onFiltersChange({ role: value })}
style={{ width: 150 }}
allowClear
>
Admin
User
Moderator
placeholder="Filter by status"
value={filters.status}
onChange={(value) => onFiltersChange({ status: value })}
style={{ width: 150 }}
>
Active
Inactive
icon={ }
onClick={resetFilters}
>
Reset Filters
icon={ }
onClick={handleRefresh}
loading={isLoading}
>
Refresh
columns={columns}
dataSource={users}
loading={isLoading}
rowKey="id"
pagination={{
current: page,
pageSize: pageSize,
total: totalCount,
onChange: changePage,
showSizeChanger: false,
showTotal: (total) => Total ${total} users,
}}
/>
);
}
`🔧 TypeScript
The hook provides full type inference when you use the exported utility types. Always import and use these types for the best TypeScript experience:
`typescript
import usePaginatedList, {
FilterablePaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';// Define your data types
interface User {
id: string;
name: string;
email: string;
}
interface UserFilters {
role?: string;
status?: string;
searchValue?: string;
}
// ✅ CORRECT: Your fetch method using exported types
const fetchUsers = async (
args: FilterablePaginatedListRequest
): Promise> => {
const params = new URLSearchParams({
page: args.page.toString(),
page_size: args.page_size.toString(),
...args.filters,
});
const response = await fetch(
/api/users?${params}, { signal: args.signal });
return response.json();
};// Hook automatically infers types correctly
const {
data, // ✅ Type: User[]
filters, // ✅ Type: UserFilters
onFiltersChange, // ✅ Type: (filters: UserFilters) => void
} = usePaginatedList({
fetchMethod: fetchUsers,
});
`$3
`typescript
// ❌ BAD: Writing types manually without imports
const fetchUsers = async (args: {
page: number;
page_size: number;
filters?: UserFilters;
signal?: AbortSignal;
}): Promise<{
results: User[];
count: number;
next: string | null;
previous: string | null;
}> => {
// This works but you lose type inference benefits
// and won't get compiler errors if the API changes
};
`$3
`typescript
import {
FilterablePaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';// ✅ GOOD: Using exported types ensures:
// - Full type inference
// - Type safety
// - IDE autocomplete
// - Compile-time error detection
const fetchUsers = async (
args: FilterablePaginatedListRequest
): Promise> => {
// Implementation
};
`🎨 Best Practices
$3
`typescript
// ✅ Good - Pure function
const fetchProducts = async (args) => {
return api.get('/products', { params: args });
};// ❌ Bad - Side effects in fetch method
const fetchProducts = async (args) => {
showLoadingToast(); // Side effect
return api.get('/products', { params: args });
};
`$3
`typescript
// ✅ Good - Explicit filter types
interface ProductFilters {
searchValue?: string;
category?: string;
status?: 'available' | 'out_of_stock';
minPrice?: number;
maxPrice?: number;
}// ❌ Bad - Any type
type ProductFilters = any;
`$3
`typescript
import { Alert, Spin } from 'antd';const { data, error, isLoading } = usePaginatedList({
fetchMethod: fetchProducts,
});
if (error) {
return (
message="Error"
description="Failed to load data. Please try again."
type="error"
showIcon
/>
);
}
if (isLoading) {
return ;
}
return
;
`$3
`typescript
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AlertCircle } from 'lucide-react';const { data, error, isLoading } = usePaginatedList({
fetchMethod: fetchProducts,
});
if (error) {
return (
Error
Failed to load data. Please try again.
);
}
if (isLoading) {
return
Loading...;
}return
;
`$3
The hook automatically handles this! When filters change, it maintains the current page. If you want to reset to page 1, do it manually:
`typescript
const { onFiltersChange, changePage } = usePaginatedList({
fetchMethod: fetchData,
});const handleFilterChange = (newFilters) => {
changePage(1); // Reset to first page
onFiltersChange(newFilters);
};
`🔍 How It Works
1. Initialization: Hook initializes with default or provided initial state
2. Auto-Fetch: Automatically fetches data when mounted
3. Filter Changes: When filters change, debounced fetch is triggered
4. Page Changes: When page changes, immediate fetch is triggered
5. Request Abortion: Previous requests are aborted when new requests start
6. State Updates: Loading, data, and error states are updated accordingly
7. Cleanup: Aborts pending requests on unmount
🚦 Common Patterns
$3
`typescript
function PaginationControls({ page, totalCount, pageSize, onPageChange }) {
const totalPages = Math.ceil(totalCount / pageSize);
return (
onClick={() => onPageChange(page - 1)}
disabled={page === 1}
>
Previous
Page {page} of {totalPages}
onClick={() => onPageChange(page + 1)}
disabled={page === totalPages}
>
Next
);
}
`$3
`typescript
import usePaginatedList, {
FilterablePaginatedListRequest,
PaginatedListResponse,
} from '@mutasimalmu/use-paginated-list-query';
import { Table, Input, Space } from 'antd';
import { SearchOutlined } from '@ant-design/icons';interface Product {
id: string;
name: string;
price: number;
category: string;
}
interface ProductFilters {
searchValue?: string;
}
async function fetchProducts(
args: FilterablePaginatedListRequest
): Promise> {
const params = new URLSearchParams({
page: args.page.toString(),
page_size: args.page_size.toString(),
...args.filters,
});
const response = await fetch(
/api/products?${params}, { signal: args.signal });
return response.json();
}function SearchableProductsTable() {
const {
data: products,
filters,
onFiltersChange,
isLoading,
page,
pageSize,
totalCount,
changePage,
} = usePaginatedList({
fetchMethod: fetchProducts,
debounceTime: 500, // Wait 500ms after user stops typing
pageSize: 10,
});
const columns = [
{ title: 'Product', dataIndex: 'name', key: 'name' },
{ title: 'Price', dataIndex: 'price', key: 'price' },
{ title: 'Category', dataIndex: 'category', key: 'category' },
];
return (
prefix={ }
placeholder="Search products..."
value={filters.searchValue || ''}
onChange={(e) => onFiltersChange({ searchValue: e.target.value })}
allowClear
/>
columns={columns}
dataSource={products}
loading={isLoading}
rowKey="id"
pagination={{
current: page,
pageSize: pageSize,
total: totalCount,
onChange: changePage,
}}
/>
);
}
`🐛 Troubleshooting
$3
- Check that disabled prop is not set to true
- Verify your fetch method signature matches the required format
- Check network tab for API errors$3
- Increase debounceTime for filter-heavy UIs
- Ensure you're not calling onFiltersChange` unnecessarilyMIT License - see LICENSE file for details
Contributions are welcome! Please feel free to submit a Pull Request.
---
Keywords: React hook, pagination, client-side pagination, data fetching, filters, TypeScript, debounce, loading states, abort controller, React Query alternative