usePagination: 3 lines of code, infinite possibilities
Source: Dev.to
Paginated lists are one of the most common patterns in frontend development. A hand-written implementation — with state management for page, pageSize, total, loading, error, and handlers for pagination, search debounce, and item mutations — easily reaches 50+ lines. alova’s usePagination hook collapses this into roughly 3 lines of configuration. This article examines how the abstraction works and where it fits. Here’s a standard paginated list written with Vue 3 and Axios: const list = ref([]); const loading = ref(false); const error = ref(null); const page = ref(1); const pageSize = ref(10); const total = ref(0); const searchKeyword = ref(”);
const fetchList = async () => { loading.value = true; error.value = null; try { const res = await axios.get(‘/api/users’, { params: { page: page.value, pageSize: pageSize.value, keyword: searchKeyword.value, }, }); list.value = res.data.data; total.value = res.data.total; } catch (e) { error.value = e.message; } finally { loading.value = false; } };
const changePage = (p) => { page.value = p; fetchList(); };
const changePageSize = (ps) => { pageSize.value = ps; page.value = 1; fetchList(); };
const onSearch = (keyword) => { searchKeyword.value = keyword; page.value = 1; fetchList(); };
const deleteItem = async (id) => { await axios.delete(/api/users/${id}); fetchList(); };
onMounted(() => fetchList());
The actual business logic — GET /api/users — is one line. The remaining 50+ lines are infrastructure: state wiring, loading toggles, pagination coordination, and filter resets. This pattern repeats in every list page across the project. usePagination internalizes all of that infrastructure: import { usePagination } from ‘alova/client’;
const searchKeyword = ref(”);
const { loading, data, error, page, pageSize, total, pageCount, isLastPage, fetching, removing, replacing, status, refresh, insert, remove, replace, reload, onSuccess, onError, onComplete, } = usePagination( (page, pageSize) => alovaInstance.Get(‘/api/users’, { params: { page, pageSize, keyword: searchKeyword.value }, }), { initialPage: 1, initialPageSize: 10, watchingStates: [searchKeyword], debounce: 300, } );
Three lines of configuration replace 50+ lines of imperative code. Here’s what you get: Modifying page.value or pageSize.value triggers a request automatically. Changing pageSize resets to page 1 — no manual wiring needed. page.value = 3; // fetch page 3 automatically pageSize.value = 20; // reset to page 1 and fetch automatically
Add reactive states to watchingStates with an optional debounce, and the list re-fetches whenever any filter changes: searchKeyword.value = ‘John’; // auto-fetch from page 1 after 300ms debounce
insert, remove, and replace update the local list and sync with the server — no manual refetch needed: await insert({ id: 99, name: ‘New User’ }, 0); // prepend await remove(2); // delete 3rd item await replace({ id: 5, name: ‘Updated’ }, 4); // replace 5th item await refresh(page.value); // force-refresh current page await reload(); // clear and reload from page 1
Next and previous pages preload in the background by default. When the user clicks “next page,” data is already in cache — no loading spinner. Beyond a single loading boolean, usePagination exposes per-operation states: loading, // current page is loading fetching, // preloading in background (doesn’t block the UI) removing, // array of row indices currently being removed replacing, // index of the row being replaced status, // current operation: “loading” | “removing” | “inserting” | “replacing”
This enables row-level loading indicators during delete operations while keeping the rest of the list interactive. The code reduction comes from elevating the abstraction level: Manual approach: You handle each HTTP request individually, managing all state transitions by hand usePagination approach: The entire “paginated list” scenario is a single configurable unit, with common logic (loading toggles, error handling, pagination coordination) baked into the hook Standard CRUD admin panels: User lists, order tables, content management — pages with pagination, search, and inline CRUD Multi-filter + pagination combos: Several filter dimensions that must reset pagination on change Frequent list item mutations: Inline edit, delete, insert operations where optimistic updates eliminate refetch overhead Standard API response shapes: { data: [], total: number } or structures configurable via data/total callbacks Cursor-based pagination: APIs using after/before cursors don’t map cleanly to the page-number model Bidirectional infinite scroll: UIs that load in both directions (e.g., chat histories) exceed the hook’s design scope Multi-list coordination: One operation must update several paginated lists with strict ordering requirements Multi-endpoint data aggregation: List data assembled from multiple APIs that can’t be mapped through data/total callbacks
Dimension Manual usePagination
Code volume 50-60 lines ~3 lines config
Pagination logic Manual page + fetch Auto-response to state changes
Search debounce Hand-rolled
watchingStates + debounce
List mutations Refetch after each operation Optimistic insert/remove/replace
Preloading Build from scratch Built-in, enabled by default
Loading granularity Single boolean Multi-level: loading/fetching/removing/replacing
Customization ceiling Fully flexible Constrained by hook design
Learning curve Framework fundamentals Understanding hook config and behavior
usePagination doesn’t do anything you couldn’t write yourself. Its value is packaging a battle-tested pagination implementation so you don’t write — and debug — the same 50 lines for every list page. For standard pagination use cases, the reduction in boilerplate and the built-in preloading and optimistic updates are measurable improvements. For scenarios outside its design scope, a custom implementation remains the clearer choice.