I’m implementing lazy loading with virtual scrolling in a PrimeVue DataTable. The data should be dynamically fetched from an API as the user scrolls. While the initial data fetch (first 25 accounts) works and displays correctly in the table, subsequent data loaded on scroll does not appear in the UI, even though the virtualAccounts
array updates correctly and matches the expected length.
I also checked the official PrimeVue documentation about lazy loading and virtual scrolling (https://primevue.org/datatable/#lazy_virtualscroll) and tried to follow everything there, but still, it doesn’t work.
Due to the size of my project (over 22,000 lines of code) and the fact that the relevant module is nearly 1,000 lines long, I can only share the essential portions of my implementation:
DataTable configuration:
<DataTable
dataKey="ID"
ref="table"
:value="virtualAccounts"
scrollable
scroll-height="calc(100vh - 23rem)"
:virtualScrollerOptions="{
lazy: true,
onLazyLoad: loadAccountsLazy,
itemSize: 46,
delay: 200,
showLoader: true,
loading: lazyLoading,
numToleratedItems: 10,
}"
/>
LazyLoading logic:
const virtualAccounts = ref(
Array.from({ length: 100 }, () => ({
ID: null,
Rating: 0,
CompanyName: "",
Remark: "",
TradeRegisterNumber: "",
CreditLimit: 0,
Revenue: 0,
FoundationYear: null,
Employees: 0,
VatID: "",
CreatedAt: "",
UpdatedAt: "",
}))
);
const lazyLoading = ref(false);
const fetchAccountsOnDemand = async (first, last) => {
try {
const response = await axios.get("/api/data/list", {
params: { first, last },
});
return response.data || { data: [] };
} catch (error) {
console.error("Error fetching data:", error);
return { data: [] };
}
};
const loadAccountsLazy = async (event) => {
lazyLoading.value = true;
try {
const { first, last } = event;
const response = await fetchAccountsOnDemand(first, last);
if (Array.isArray(response.data)) {
const _virtualAccounts = [...virtualAccounts.value];
for (let i = first; i < last; i++) {
_virtualAccounts[i] = { ID: null, Rating: 0, CompanyName: "", Remark: "", TradeRegisterNumber: "", CreditLimit: 0, Revenue: 0, FoundationYear: null, Employees: 0, VatID: "", CreatedAt: "", UpdatedAt: "" };
}
const processedData = response.data.map((account) => ({
ID: account.ID || null,
Rating: account.Rating || 0,
CompanyName: account.CompanyName || "",
Remark: account.Remark || "",
TradeRegisterNumber: account.TradeRegisterNumber || "",
CreditLimit: account.CreditLimit || 0,
Revenue: account.Revenue || 0,
FoundationYear: account.FoundationYear || null,
Employees: account.Employees || 0,
VatID: account.VatID || "",
CreatedAt: account.CreatedAt || "",
UpdatedAt: account.UpdatedAt || "",
}));
_virtualAccounts.splice(first, processedData.length, ...processedData);
virtualAccounts.value = JSON.parse(JSON.stringify(_virtualAccounts));
}
} catch (error) {
console.error("Error during lazy loading:", error);
} finally {
lazyLoading.value = false;
}
};
Initial data fetch:
onMounted(async () => {
try {
const initialData = await fetchAccountsOnDemand(0, 25);
if (Array.isArray(initialData.data)) {
const processedData = initialData.data.map((account, i) => ({
ID: account.ID || i++,
Rating: account.Rating || 0,
CompanyName: account.CompanyName || "",
Remark: account.Remark || "",
TradeRegisterNumber: account.TradeRegisterNumber || "",
CreditLimit: account.CreditLimit || 0,
Revenue: account.Revenue || 0,
FoundationYear: account.FoundationYear || null,
Employees: account.Employees || 0,
VatID: account.VatID || "",
CreatedAt: account.CreatedAt || "",
UpdatedAt: account.UpdatedAt || "",
}));
virtualAccounts.value = Array(100).fill({
ID: null,
Rating: 0,
CompanyName: "",
Remark: "",
TradeRegisterNumber: "",
CreditLimit: 0,
Revenue: 0,
FoundationYear: null,
Employees: 0,
VatID: "",
CreatedAt: "",
UpdatedAt: "",
});
virtualAccounts.value.splice(0, processedData.length, ...processedData);
console.log("MOUNT:", virtualAccounts.value);
}
} catch (error) {
console.error("Error during initial data fetch:", error);
}
});
To troubleshoot further, I even tested the virtual scrolling functionality with a testing component where the data is generated entirely in the frontend (no backend calls). In that case, the virtual scrolling works perfectly, suggesting the issue lies somewhere in the integration with the backend-fetching logic or how the virtualAccounts
array is updated/reactive.
Working example:
<template>
<div class="card">
<DataTable
:value="virtualCars"
scrollable
scrollHeight="500px"
tableStyle="min-width: 50rem"
:virtualScrollerOptions="{
lazy: true,
onLazyLoad: loadCarsLazy,
itemSize: 46,
delay: 200,
showLoader: true,
loading: lazyLoading,
numToleratedItems: 10,
}"
>
<Column field="id" header="Id" style="width: 20%">
<template #loading>
<div
class="flex items-center"
:style="{ height: '17px', 'flex-grow': '1', overflow: 'hidden' }"
>
<Skeleton width="60%" height="1rem" />
</div>
</template>
</Column>
<Column field="vin" header="Vin" style="width: 20%">
<template #loading>
<div
class="flex items-center"
:style="{ height: '17px', 'flex-grow': '1', overflow: 'hidden' }"
>
<Skeleton width="40%" height="1rem" />
</div>
</template>
</Column>
<Column field="year" header="Year" style="width: 20%">
<template #loading>
<div
class="flex items-center"
:style="{ height: '17px', 'flex-grow': '1', overflow: 'hidden' }"
>
<Skeleton width="30%" height="1rem" />
</div>
</template>
</Column>
<Column field="brand" header="Brand" style="width: 20%">
<template #loading>
<div
class="flex items-center"
:style="{ height: '17px', 'flex-grow': '1', overflow: 'hidden' }"
>
<Skeleton width="40%" height="1rem" />
</div>
</template>
</Column>
<Column field="color" header="Color" style="width: 20%">
<template #loading>
<div
class="flex items-center"
:style="{ height: '17px', 'flex-grow': '1', overflow: 'hidden' }"
>
<Skeleton width="60%" height="1rem" />
</div>
</template>
</Column>
</DataTable>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
const cars = ref([]);
const virtualCars = ref(Array.from({ length: 100000 }));
const lazyLoading = ref(false);
const loadLazyTimeout = ref();
// Generate car data
const generateCar = (id) => {
const brands = ["Toyota", "Ford", "BMW", "Honda", "Mazda"];
const colors = ["Red", "Blue", "Green", "Black", "White"];
return {
id,
vin: `VIN${id}`,
year: 2000 + (id % 23), // Random year from 2000 to 2023
brand: brands[id % brands.length],
color: colors[id % colors.length],
};
};
onMounted(() => {
cars.value = Array.from({ length: 100000 }).map((_, i) => generateCar(i + 1));
});
const loadCarsLazy = (event) => {
!lazyLoading.value && (lazyLoading.value = true);
if (loadLazyTimeout.value) {
clearTimeout(loadLazyTimeout.value);
}
// Simulate remote connection with a timeout
loadLazyTimeout.value = setTimeout(() => {
let _virtualCars = [...virtualCars.value];
let { first, last } = event;
console.log(event);
// Load data of required page
const loadedCars = cars.value.slice(first, last);
// Populate page of virtual cars
Array.prototype.splice.apply(_virtualCars, [
...[first, last - first],
...loadedCars,
]);
virtualCars.value = _virtualCars;
lazyLoading.value = false;
}, Math.random() * 1000 + 250);
};
</script>
Issue:
-
Initial Load: The first 25 records fetch and display correctly.
-
Subsequent Loads: When scrolling to load more data, the
virtualAccounts
array updates as expected, but the UI does not reflect the changes.
Troubleshooting steps:
-
Verified that the
virtualAccounts
array updates correctly with the fetched data. -
Confirmed that the DataTable renders correctly when using a frontend-only example with generated data.
-
Ensured I followed the PrimeVue documentation for lazy loading and virtual scrolling.
Questions:
-
Are there any specific steps missing in my implementation based on the PrimeVue guidelines?
-
Could the issue be related to how the
virtualAccounts
array is updated or its reactivity?