Server‑Side Pagination in Quasar QTable: Keep the Browser Light
When datasets grow, a Quasar QTable that renders every row in the browser becomes sluggish. This post shows how to use QTable’s request event to delegate slicing, sorting, and total counts to a backend API, keeping the client lean and responsive.
03 May 2026, 17:02 UTC

Problem: Client‑Side Tables Get Heavy
In a demo QTable you’ll often see all rows loaded into the browser, then sliced by the component when the user changes page. That works fine for a handful of rows, but as soon as you hit a few thousand, the browser has to keep the entire array in memory, re‑render the DOM, and calculate totals, causing lag and high memory usage.
Why Client‑Side Fails
- All rows are fetched in one request.
- Sorting, filtering, and paging are done in JavaScript.
- Large tables can freeze the UI or exhaust JS heap.
Server‑Side Solution with QTable’s Request Contract
Quasar’s QTable exposes a request event that fires whenever the pagination object changes. The event handler receives the current pagination object and expects you to provide the data slice and the total row count.
The pagination object contains:
| Property | Description |
|---|---|
page | Current page index (1‑based). |
rowsPerPage | Number of rows to display per page. |
sortBy | Field name to sort on. |
descending | Boolean indicating sort direction. |
rowsNumber | Total number of rows in the dataset (optional). |
Concrete Example: Fetching a Page from a REST API
Below is a minimal component that uses QTable with server‑side pagination. Replace <API_URL> with your endpoint and adjust the query parameters to match your back‑end contract.
// MyTable.vue
<template>
<q-table
:data="rows"
:columns="columns"
:pagination="pagination"
:loading="loading"
@request="onRequest"
row-key="id"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const columns = [
{ name: 'id', label: 'ID', field: 'id', sortable: true },
{ name: 'name', label: 'Name', field: 'name', sortable: true },
// add more columns as needed
];
const rows = ref([]);
const loading = ref(false);
const pagination = ref({
page: 1,
rowsPerPage: 25,
sortBy: 'id',
descending: false,
rowsNumber: 0, // will be updated from server
});
async function onRequest(p) {
loading.value = true;
try {
const { page, rowsPerPage, sortBy, descending } = p;
const response = await fetch(`${process.env.VUE_APP_API_URL}/items?` +
new URLSearchParams({
limit: rowsPerPage.toString(),
offset: ((page - 1) * rowsPerPage).toString(),
sortBy,
order: descending ? 'desc' : 'asc',
}));
const data = await response.json(); // expected { rows: [...], rowsNumber: 1234 }
rows.value = data.rows;
pagination.value.rowsNumber = data.rowsNumber;
} catch (err) {
console.error('Failed to fetch table data', err);
} finally {
loading.value = false;
}
}
</script>
Key points:
- Use
loadingto show a spinner while the request is in flight. - The API must return an object with
rows(array of row objects) androwsNumber(total count). - Do not modify
rowsNumberlocally; it comes from the server. - When the user changes page, sort, or filter, the
requestevent fires again with the new pagination state.
Handling Rapid Changes and Stale Responses
If a user flips pages quickly, earlier requests may finish after later ones, overwriting the table with stale data. Two common mitigations:
- AbortController – create a new controller for each request and abort the previous one.
- Sequence Guard – keep a monotonically increasing request counter and discard responses that arrive out of order.
Both approaches prevent the UI from showing inconsistent data, especially when network latency is variable.
Trade‑offs & Limitations
- Backend Load: All sorting, filtering, and paging logic runs on the server. Ensure your database indexes support the expected queries.
- rowsNumber Accuracy: The table relies on an exact total count. If the underlying data changes between requests, the pagination UI may show an incorrect number of pages. Use optimistic or stale‑while‑revalidate strategies if consistency is critical.
- Security: Expose only sanitized query parameters to the back‑end. Never interpolate raw
sortByvalues directly into SQL statements. - Version Sensitivity: Quasar v1 (Vue 2) uses
:pagination.sync; Quasar v2 (Vue 3) usesv-model:pagination. Verify your project’s major version before copying the snippet.
Checklist Before You Deploy
- Confirm the API returns
rowsandrowsNumberin JSON. - Test with a realistic dataset size to ensure latency is acceptable.
- Add
AbortControlleror sequence guard if users frequently change pages. - Validate
sortByand filter values on the server to avoid injection. - Measure memory usage in the browser; it should stay near the size of the current page.
Conclusion
Server‑side pagination turns QTable from a heavy, client‑side renderer into a lightweight viewport over your data store. By delegating slicing, sorting, and counting to the back‑end, you keep the browser responsive even with millions of rows. Just remember to handle stale responses, secure your query parameters, and verify that rowsNumber remains accurate. Once those pieces are in place, you’ll have a scalable, performant table that feels fast to the user.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.