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.
The Hand-Written Baseline
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: Configuration Over Boilerplate
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:
1. Automatic pagination
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
2. Debounced search with filter watching
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
3. Optimistic list mutations
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
4. Adjacent-page preloading
Next and previous pages preload in the background by default. When the user clicks "next page," data is already in cache — no loading spinner.
5. Granular operation states
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 Core Abstraction
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
When usePagination Is a Good Fit
- 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 viadata/totalcallbacks
When to Look Elsewhere
-
Cursor-based pagination: APIs using
after/beforecursors 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/totalcallbacks
Comparison at a Glance
| 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 |
Summary
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.
Top comments (0)