Efficient Data Grids with Vuetify 3: Server‑Side Pagination & Sorting
Vuetify 3’s VDataTable supports server‑side pagination, sorting, and filtering. This guide shows how to wire the table to a backend, fetch only the visible slice, and handle loading state, with a practical Composition‑API example and trade‑off discussion.
22 Sept 2025, 09:43 UTC

Why Client‑Side Pagination Falls Short
When a table contains thousands of rows, loading the entire dataset into the browser can cause long initial load times, high memory usage, and sluggish scrolling. Even if you slice the data in the component, the browser still parses each row, which hurts performance. Server‑side pagination solves this by asking the backend for only the rows that belong to the current page.
Vuetify 3’s Server‑Side API
VDataTable exposes a small set of props and events that let you swap out the default client‑side logic for a remote‑data approach. The key pieces are listed in the table below.
| Prop / Event | Purpose |
|---|---|
server-items-length | Tell the table the total number of items available on the server. |
items-per-page | Default page size shown in the UI. |
sort-by | Column key to sort by. |
sort-desc | Boolean indicating descending order. |
loading | Show a skeleton loader while the API call is in flight. |
onUpdate:page | Event emitted when the user changes the page. |
onUpdate:sortBy | Event emitted when the user changes the sort column. |
onUpdate:sortDesc | Event emitted when the user toggles sort direction. |
Putting It All Together – A Composition‑API Example
Below is a minimal, self‑contained example that demonstrates how to wire the table to a mock API. It uses the useVDataTable composable introduced in Vuetify 3 to reduce boilerplate.
import { ref, computed, watch } from 'vue'
import { useVDataTable } from 'vuetify/lib/composables'
export default {
setup() {
// Reactive state for the table
const items = ref([])
const loading = ref(false)
const totalItems = ref(0)
// Table options that mirror the VDataTable props
const page = ref(1)
const itemsPerPage = ref(10)
const sortBy = ref('name')
const sortDesc = ref(false)
// Helper that returns the query string for the API
const buildQuery = () => {
const params = new URLSearchParams()
params.append('page', page.value)
params.append('itemsPerPage', itemsPerPage.value)
params.append('sortBy', sortBy.value)
params.append('sortDesc', sortDesc.value)
return params.toString()
}
// Fetch data from the server
const fetchData = async () => {
loading.value = true
try {
const res = await fetch(`<API_ENDPOINT>?${buildQuery()}`)
const json = await res.json()
items.value = json.data
totalItems.value = json.total
} finally {
loading.value = false
}
}
// Call fetchData whenever pagination or sorting changes
watch([page, itemsPerPage, sortBy, sortDesc], fetchData, { immediate: true })
// Expose props and events for the template
const tableProps = computed(() => ({
items: items.value,
'server-items-length': totalItems.value,
'items-per-page': itemsPerPage.value,
'sort-by': sortBy.value,
'sort-desc': sortDesc.value,
loading: loading.value,
}))
const tableEvents = {
'update:page': (p) => (page.value = p),
'update:sortBy': (s) => (sortBy.value = s),
'update:sortDesc': (d) => (sortDesc.value = d),
}
return { tableProps, tableEvents }
},
}
In the template you would then bind v-bind and v-on to the computed objects:
<v-data-table
v-bind="tableProps"
v-on="tableEvents"
:headers="[{ text: 'Name', value: 'name' }, { text: 'Age', value: 'age' }]"
>
<template #item.name>{{ item.name }}</template>
</v-data-table>
Diagnosing Common Pitfalls
- Backend Returns Full Dataset – Verify the API endpoint accepts the query parameters. Inspect the network tab for a request like
?page=2&itemsPerPage=10. The response should contain only 10 rows. - Pagination Controls Out of Sync – Ensure
server-items-lengthaccurately reflects the total count returned by the server. If the table shows fewer pages than expected, the total is likely wrong. - Sorting Doesn’t Persist Across Pages – The sort props must be sent with every request. If the backend ignores
sortBy, the table will appear unsorted.
Performance Trade‑offs
- Debouncing API Calls – Rapid page changes can flood the server. Wrap
fetchDatain a debounce (e.g., 200 ms) or usewatchEffectwith a delay. - Backend Query Cost – Sorting and filtering on large tables can be expensive. Make sure the database indexes the columns used in
sortByandfilterBy. - Skeleton vs. Spinner –
loadingtriggers Vuetify’s skeleton loader, which is lighter than a full spinner and keeps the table layout stable.
Actionable Checklist
- Expose
totalanddatafields in the API response. - Set
server-items-lengthto the API‑provided total. - Bind
onUpdate:page,onUpdate:sortBy, andonUpdate:sortDescto trigger data fetches. - Use
loadingto show a skeleton while waiting for the response. - Test by changing pages and sort columns; confirm network requests contain the correct query string.
- Monitor backend logs for query performance; add indexes as needed.
Conclusion
Server‑side pagination, sorting, and filtering in Vuetify 3’s VDataTable let you build responsive, high‑performance data grids that scale to thousands of rows. By wiring the table’s props and events to a Composition‑API state and a well‑indexed backend, you avoid client‑side bottlenecks while keeping the UI fast and responsive. Follow the checklist above to ensure a smooth integration and to catch the most common pitfalls early.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.