Frontend Development 12 min read

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.

政采云技术
政采云技术
政采云技术
Resolving Duplicate Items in a Waterfall Flow Component with Asynchronous Rendering Control

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.

frontendmini-programasynchronous renderingduplicate itemspromise queueWaterfall Layout
政采云技术
Written by

政采云技术

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.