usePaginatedFetch
A powerful composable for fetching paginated data with support for search, filtering, and infinite scrolling. This composable handles common data fetching patterns automatically, including debounced search, abort controllers for canceling outdated requests, and tracking pagination state.
Usage
ts
import { ref } from 'vue'
import { usePaginatedFetch } from '#imports'
// Basic usage with required parameters
const resource = ref('api/users')
const search = ref('')
const extraParams = ref({})
const paginate = ref(true)
const lazy = ref(false)
const { items, page, hasMore, loading, loadMore } = usePaginatedFetch(
resource,
{
search,
extraParams,
paginate,
lazy,
onError: (error) => console.error(error)
}
)
Type Signature
ts
function usePaginatedFetch(
resource: Ref<string>,
options: PaginatedFetchParams
): {
items: Ref<any[]>;
page: Readonly<Ref<number>>;
hasMore: Ref<boolean>;
loading: Ref<boolean>;
loadMore: () => Promise<void>;
}
interface PaginatedFetchParams {
search: Ref<string>;
extraParams: Ref<Record<string, any>>;
paginate: Ref<PaginateOptions>;
lazy: Ref<boolean>;
onError: (error: any) => void;
}
type PaginateOptions = boolean | { limit: number };
Parameters
resource
- Type:
Ref<string>
- Description: The API endpoint to fetch data from
options
Parameter | Type | Description |
---|---|---|
search | Ref<string> | Search query string (debounced automatically) |
extraParams | Ref<Record<string, any>> | Additional query parameters |
paginate | Ref<boolean | { limit: number }> | Pagination settings - true or configuration object |
lazy | Ref<boolean> | Whether to use lazy loading (caching) for requests |
onError | (error: any) => void | Error handling callback |
Return Value
Property | Type | Description |
---|---|---|
items | Ref<any[]> | The fetched data items |
page | Readonly<Ref<number>> | Current page number (readonly) |
hasMore | Ref<boolean> | Whether there are more pages to load |
loading | Ref<boolean> | Whether a request is in progress |
loadMore | () => Promise<void> | Function to load the next page of data |
Features
- Automatic request cancellation: Outdated requests are aborted when parameters change
- Debounced search: Search requests are debounced to prevent excessive API calls
- Infinite scrolling support:
loadMore
function appends new items to the existing list - Deep parameter watching: Automatically reloads when
extraParams
change - Lazy loading support: Optional caching of requests when appropriate
Examples
Basic Table with Pagination
vue
<script setup>
import { ref } from 'vue'
import { usePaginatedFetch } from '#imports'
const resource = ref('api/products')
const search = ref('')
const extraParams = ref({ category: 'electronics' })
const paginate = ref(true)
const lazy = ref(false)
const { items, loading, page, hasMore, loadMore } = usePaginatedFetch(
resource,
{ search, extraParams, paginate, lazy, onError: console.error }
)
</script>
<template>
<div>
<input v-model="search" placeholder="Search..." />
<table>
<thead>
<tr>
<th>Name</th>
<th>Price</th>
</tr>
</thead>
<tbody>
<tr v-for="item in items" :key="item.id">
<td>{{ item.name }}</td>
<td>{{ item.price }}</td>
</tr>
</tbody>
</table>
<div v-if="loading">Loading...</div>
<button v-if="hasMore" @click="loadMore">Load more</button>
</div>
</template>
Infinite Scrolling List
vue
<script setup>
import { ref, onMounted } from 'vue'
import { usePaginatedFetch } from '#imports'
const resource = ref('api/messages')
const search = ref('')
const extraParams = ref({})
const paginate = ref({ limit: 20 })
const lazy = ref(false)
const { items, loading, hasMore, loadMore } = usePaginatedFetch(
resource,
{ search, extraParams, paginate, lazy, onError: console.error }
)
// Intersection Observer for infinite scrolling
let observer
onMounted(() => {
observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore.value && !loading.value) {
loadMore()
}
})
if (document.querySelector('.scroll-trigger')) {
observer.observe(document.querySelector('.scroll-trigger'))
}
})
</script>
<template>
<div class="messages-container">
<div v-for="item in items" :key="item.id" class="message-item">
<h3>{{ item.title }}</h3>
<p>{{ item.content }}</p>
</div>
<div v-if="loading" class="loading">Loading more messages...</div>
<div v-if="hasMore" class="scroll-trigger"><!-- Invisible element for observer --></div>
</div>
</template>
Filtering with Multiple Parameters
vue
<script setup>
import { ref, reactive } from 'vue'
import { usePaginatedFetch } from '#imports'
const resource = ref('api/users')
const search = ref('')
const filters = reactive({
role: '',
status: 'active',
department: ''
})
const extraParams = ref({ ...filters })
const paginate = ref(true)
const lazy = ref(false)
const { items, loading } = usePaginatedFetch(
resource,
{
search,
extraParams,
paginate,
lazy,
onError: (err) => console.error('Failed to fetch users:', err)
}
)
function applyFilters() {
extraParams.value = { ...filters }
}
function resetFilters() {
Object.keys(filters).forEach(key => { filters[key] = '' })
filters.status = 'active'
applyFilters()
}
</script>
<template>
<div>
<div class="search-filters">
<input v-model="search" placeholder="Search by name or email" />
<select v-model="filters.role">
<option value="">All Roles</option>
<option value="admin">Admin</option>
<option value="user">User</option>
<option value="guest">Guest</option>
</select>
<select v-model="filters.status">
<option value="">All Status</option>
<option value="active">Active</option>
<option value="inactive">Inactive</option>
</select>
<button @click="applyFilters">Apply Filters</button>
<button @click="resetFilters">Reset</button>
</div>
<div v-if="loading">Loading...</div>
<ul class="users-list">
<li v-for="user in items" :key="user.id">
{{ user.name }} - {{ user.email }} ({{ user.role }})
</li>
</ul>
</div>
</template>