Fixing Vuetify v-data-table: Server Pagination and Sorting Not Updating the UI
Your v-data-table fires the right request but never re-renders new pages or sort orders. Diagnose the options binding, server-items-length, and version-specific syntax that cause stale server-driven tables.
05 Jun 2026, 18:50 UTC

You click page 2 of your v-data-table, the network request fires, the server returns the right rows — and the table still shows page 1's data. Or sorting flips the arrow but the rows never reorder. This is the classic symptom of server-driven pagination state that never makes it back into the table component. The fix is almost always in how you bind the options object and total item count, not in your API.
Recognizing the condition
You have this problem if any of the following hold:
- Changing pages or sort order triggers a request, but the rendered rows don't change.
- The pagination footer shows the wrong total number of pages (often 1, or a count based only on the current page's rows).
- The table silently falls back to slicing the current page's items client-side, so every page shows the same subset.
Cause and diagnostic table
| Symptom | Likely cause | Where to look |
|---|---|---|
| Rows never change on page/sort | Options object not bound two-way; handlers overwrite it with a fresh object | v-model:options (Vuetify 3) or :options.sync (Vuetify 2) |
| Footer shows only one page | Total count not passed to the table | :server-items-length prop and your API response |
| Request fires but has wrong page/sort params | Handler reads a stale copy of options | The @update:options handler body |
| Table sorts only the visible rows | Client-side pagination active unintentionally | Missing server-items-length, or disable-pagination/disable-sort set |
| Works locally, breaks after upgrade | Mixed Vuetify 2 / Vuetify 3 syntax | Prop names and the .sync modifier |
Ordered checks
- Inspect the table's props in your template. Confirm you have a two-way options binding and a server item count. For Vuetify 3 (Vue 3):
v-model:options="options"and:server-items-length="totalItems". For Vuetify 2 (Vue 2)::options.sync="options"and:server-items-length="totalItems". - Open DevTools → Network and change the page. Confirm the outgoing request carries the page, items-per-page, and sort values you expect. If the request always sends page 1, the options object isn't updating — the problem is in your binding, not the server.
- Inspect the response. It must contain both the row array and a total count across all pages (not the count of the current page). If the total is missing or wrong, the footer and page math will be wrong no matter how the table is configured.
- Check the options object in Vue DevTools. After clicking page 2,
options.pageshould be 2. If it snaps back to 1, something is recreating the object on each render — a common cause is initializing options inline in the template or resetting it in a watcher. - Look for disabling props.
disable-pagination,disable-sort, ordisable-initial-sortset unintentionally will produce exactly this "dead control" behavior.
Fixes tied to each finding
Binding and handler (Vuetify 3 / Vue 3)
The .sync modifier is removed in Vue 3, so use v-model:options. Keep a single reactive options object and let the table's update event drive your fetch:
<template>
<v-data-table-server
v-model:options="options"
:items="items"
:server-items-length="totalItems"
:loading="loading"
:headers="headers"
@update:options="loadItems"
/>
</template>
<script setup>
import { ref } from 'vue'
const options = ref({ page: 1, itemsPerPage: 10, sortBy: [] })
const items = ref([])
const totalItems = ref(0)
const loading = ref(false)
async function loadItems(newOptions) {
loading.value = true
const { page, itemsPerPage, sortBy } = newOptions
const res = await fetch(
`/api/users?page=${page}&perPage=${itemsPerPage}` +
`&sort=${sortBy[0]?.key ?? ''}&dir=${sortBy[0]?.order ?? ''}`
).then(r => r.json())
items.value = res.items // current page's rows only
totalItems.value = res.total // total across ALL pages
loading.value = false
}
</script>
Run this in your own application; the endpoint shape (items + total) is a placeholder — match whatever your API actually returns. The key points: the server must return the grand total, and you must not replace options with a new object inside the handler.
Vuetify 2 differences
In Vuetify 2 the same pattern uses :options.sync="options" and the sort fields are options.sortBy (array of keys) plus options.sortDesc (array of booleans). The component is plain v-data-table — v-data-table-server only exists in Vuetify 3 (it became stable in the 3.x line; check your exact minor version if the component is unresolved). Mixing the two syntaxes after an upgrade is the most common cause of "it worked before" reports.
Server contract
If the footer shows one page, your API is probably returning only the page's rows or a count of those rows. Return the filtered-but-unpaginated total, e.g. { "items": [...], "total": 243 }. Verify in DevTools that total stays constant while paging.
Client-side fallback
If you don't need server pagination, remove server-items-length and pass the full dataset to :items. The table will paginate and sort in the browser. This is also a valid temporary workaround while you fix the server path.
Escalation criteria
If rows stay stale after you've verified the binding, the request parameters, and the response total:
- Confirm your Vuetify major version (
npm list vuetify) and re-read the data table docs for that version — prop and event names changed between 2 and 3. - Search the Vuetify GitHub issues for your exact version; several pagination regressions have been version-specific and fixed in patch releases.
- As a last resort, pin to the previous working minor version or switch to client-side pagination to unblock the release.
Verifying the fix
Change the page and confirm in DevTools → Network that the request carries the new page and the response contains the new rows plus the correct total. The table should render the new subset and the footer should highlight the new page. In Vue DevTools, watch options.page and options.sortBy change as you interact. If you temporarily remove server-items-length and the stale behavior returns, that confirms the prop is doing its job.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.