Skip to content

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

ParameterTypeDescription
searchRef<string>Search query string (debounced automatically)
extraParamsRef<Record<string, any>>Additional query parameters
paginateRef<boolean | { limit: number }>Pagination settings - true or configuration object
lazyRef<boolean>Whether to use lazy loading (caching) for requests
onError(error: any) => voidError handling callback

Return Value

PropertyTypeDescription
itemsRef<any[]>The fetched data items
pageReadonly<Ref<number>>Current page number (readonly)
hasMoreRef<boolean>Whether there are more pages to load
loadingRef<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>

Source Code