Implementing Server-Side Pagination and Sorting in Vuetify V-DataTable
Learn how to implement server-side pagination and sorting in Vuetify's V-DataTable to handle large datasets without crashing the browser.
10 Apr 2026, 03:25 UTC

The Problem: Client-Side Memory Exhaustion
By default, the Vuetify V-DataTable processes sorting, filtering, and pagination locally. While efficient for a few hundred rows, this approach fails when datasets grow to thousands or millions of records. Loading an entire database table into the browser causes significant memory overhead, slow initial page loads, and potential browser crashes.
The solution is to shift data processing to the backend, using the V-DataTable as a stateless view that requests only the specific "window" of data currently visible to the user.
Smallest Suitable Design
To implement server-side logic, you must disable the component's internal data processing and synchronize its state with an API. The minimal architecture requires three reactive bindings and one total-count property.
Required Props and Bindings:
items: A reactive array containing only the current page of data.items-length: The total number of records existing on the server (used to calculate the number of pages).v-model:page: Tracks the current active page.v-model:items-per-page: Tracks how many records to request per page.v-model:sort-by: An array of objects defining the current sort column and direction.
Implementation Example
<template>
<v-data-table
v-model:page="options.page"
v-model:items-per-page="options.itemsPerPage"
v-model:sort-by="options.sortBy"
:headers="headers"
:items="serverItems"
:items-length="totalItems"
:loading="loading"
></v-data-table>
</template>
<script setup>
import { ref, reactive, watch } from 'vue';
const serverItems = ref([]);
const totalItems = ref(0);
const loading = ref(false);
const options = reactive({
page: 1,
itemsPerPage: 10,
sortBy: [],
});
async function loadItems() {
loading.value = true;
try {
// Map Vuetify state to API parameters
const params = {
offset: (options.page - 1) * options.itemsPerPage,
limit: options.itemsPerPage,
sortField: options.sortBy[0]?.key || 'id',
sortOrder: options.sortBy[0]?.order || 'asc',
};
const response = await fetch(`/api/data?${new URLSearchParams(params)}`);
const { data, total } = await response.json();
serverItems.value = data;
totalItems.value = total;
} finally {
loading.value = false;
}
}
// Trigger reload when pagination or sorting changes
watch(
() => [options.page, options.itemsPerPage, options.sortBy],
() => loadItems(),
{ deep: true }
);
</script>This example runs in a Vue 3 single-file component using the Composition API. The API endpoint is a placeholder; adapt the parameter names to your backend contract. The watcher fires on any change to pagination or sorting state, so no manual event handlers are needed.
Trust and Data Boundaries
In this architecture, the frontend is a requestor, not a processor. The boundary of truth resides entirely within the database.
- Data Windowing: The
itemsprop should never contain the full dataset. It should only hold the slice defined by thelimitandoffset. - Sorting Authority: The
v-model:sort-byarray tells the server how to sort, but the server must validate that the requested column is actually sortable to prevent SQL injection or API errors. - Filtering Conflict: Client-side filtering must be avoided. If a user filters the table, the request must be sent to the server; otherwise, the table will filter the current page of 10 items rather than the total dataset of 10,000.
Operational Checks and Verification
To verify the implementation is functioning as a server-side table and not a client-side one, perform these checks:
- Network Inspection: Open Browser DevTools (Network tab). Change the page number. You should see a new XHR/Fetch request. If no request is sent, the table is likely processing data locally.
- Payload Validation: Ensure the request URL contains the correct offset. For page 2 with 10 items per page, the offset should be 10.
- Footer Sync: Verify that the
V-DataTablefooter displays the correct range (e.g., "1-10 of 500") based on theitems-lengthvalue. - Sort Request Check: Click a sortable column header and confirm the new request carries the expected sort key and direction.
Failure Modes and Design Limitations
Race Conditions: If a user clicks the "Next Page" button rapidly, multiple API requests may be in flight. If the second request returns before the first, the UI may flicker or display the wrong page. Mitigation: Use an AbortController to cancel previous requests when loadItems is called.
Deep Paging Performance: As the offset increases (e.g., page 10,000), database performance often degrades because the engine must scan through all previous rows. Design Change: If your dataset is massive, replace offset-based pagination with cursor-based pagination (using a unique ID from the last record of the previous page).
State Reset: When applying a new filter or search term, you must manually reset options.page = 1. Failing to do so may result in the server returning an empty array if the new filtered result set is smaller than the current page offset.
Prop names and sort object shapes described here reflect Vuetify 3 conventions; confirm against the documentation for your installed version before shipping.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.