DEV Community

Cover image for usePagination: 3 lines of code, infinite possibilities
Scott Hu
Scott Hu

Posted on

usePagination: 3 lines of code, infinite possibilities

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());
Enter fullscreen mode Exit fullscreen mode

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,
  }
);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 via data/total callbacks

When to Look Elsewhere

  • 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

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)