Resolving Duplicate Items in a Waterfall Flow Component with Asynchronous Rendering Control
This article explains why duplicate products appear in a waterfall‑flow layout of a mini‑program, analyzes the root cause of concurrent asynchronous renders, and presents two solutions—using a flag and a promise‑based queue—to ensure only one dataRender runs at a time.
Background
Users reported that scrolling the product list sometimes caused duplicate items to appear. The page uses a waterfall‑flow layout where items are placed in two columns based on column height.
Waterfall Flow Component
What is a Waterfall Flow Component
The component displays products in an uneven multi‑column layout, as shown in the accompanying screenshots.
How to Implement a Waterfall Flow Component
The implementation uses a two‑column layout and, on each insertion, compares the heights of the left and right columns to decide where to place the next item. After each insertion the column heights are recalculated.
// dataList is the whole product list; loading more data triggers watch
watch(() => props.dataList, (newList) => {
dataRender(newList);
}, { immediate: true });
const dataRender = async (newList) => {
let leftHeight = await getViewHeight('#left');
let rightHeight = await getViewHeight('#right');
const tempList = newVal.slice(lastIndex.value, newVal.length);
for await (const item of tempList) {
leftHeight <= rightHeight ? leftDataList.value.push(item) : rightDataList.value.push(item);
await nextTick();
leftHeight = await getViewHeight('#left');
rightHeight = await getViewHeight('#right');
}
lastIndex.value = newList.length;
};When the user scrolls to the bottom, a new page of data is loaded, dataList changes, and the component watches this change to invoke dataRender . The function recalculates column heights and inserts the next item into the shorter column.
Cause of Duplicate Items
The code assumes DOM rendering finishes before the next dataRender starts, but because rendering is asynchronous, a user can trigger another load before the previous render completes. Multiple dataRender executions share the same global leftDataList , rightDataList and lastIndex , leading to index mis‑alignment and duplicate items.
// Normal flow
list = [1,2,3,4,5];
lastIndex = 5;
// Load next page
list = [1,2,3,4,5,6,7,8,9,10];
list.slice(lastIndex, list.length); // [6,7,8,9,10]
// If dataRender runs concurrently, lastIndex may still be 5
list.slice(lastIndex, list.length); // [1,2,3,4,5,6,7,8,9,10]Solution
Ensure that only one dataRender runs at a time.
Method 1 – Flag (Complex, Not Recommended)
Introduce a global flag fallLoad that is set to false while a render is in progress and reset to true after the last item is rendered. This prevents overlapping renders but may drop fast‑scroll requests.
const fallLoad = ref(true);
watch(() => {
if (fallLoad.value) {
dataRender();
fallLoad.value = false;
}
});
const dataRender = async () => {
let i = 0;
const tempList = newVal.slice(lastIndex.value, newVal.length);
for await (const item of tempList) {
i++;
leftHeight <= rightHeight ? leftDataList.value.push(item) : rightDataList.value.push(item);
await nextTick();
leftHeight = await getViewHeight('#left');
rightHeight = await getViewHeight('#right');
if ((tempList.length - 1) === i) {
fallLoad.value = true;
}
}
lastIndex.value = newList.length;
};Method 2 – Promise + Queue (Elegant, Recommended)
Wrap each render call in a task queue that executes tasks sequentially using promises. The queue stores async functions, starts execution when idle, and recursively processes the next task after the current one resolves.
class asyncQueue {
constructor() {
this.asyncList = [];
this.inProgress = false;
}
add(asyncFunc) {
return new Promise((resolve, reject) => {
this.asyncList.push({ asyncFunc, resolve, reject });
if (!this.inProgress) this.execute();
});
}
execute() {
if (this.asyncList.length > 0) {
const currentAsyncTask = this.asyncList.shift();
currentAsyncTask.asyncFunc()
.then(result => { currentAsyncTask.resolve(result); this.execute(); })
.catch(error => { currentAsyncTask.reject(error); this.execute(); });
this.inProgress = true;
} else {
this.inProgress = false;
}
}
}
export default asyncQueue;Usage:
const queue = new asyncQueue();
watch(() => props.dataList, async (newVal) => {
queue.add(() => dataRender(newVal));
}, { immediate: true, deep: true });This approach guarantees that each dataRender runs to completion before the next begins, eliminating duplicate items while preserving all user‑initiated loads.
Summary
Duplicate products in a waterfall‑flow component arise from concurrent asynchronous renders that share mutable state. Controlling concurrency with either a simple flag or, preferably, a promise‑based queue ensures orderly execution, prevents data corruption, and provides a scalable pattern for similar high‑frequency asynchronous tasks in frontend development.
政采云技术
ZCY Technology Team (Zero), based in Hangzhou, is a growth-oriented team passionate about technology and craftsmanship. With around 500 members, we are building comprehensive engineering, project management, and talent development systems. We are committed to innovation and creating a cloud service ecosystem for government and enterprise procurement. We look forward to your joining us.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.