Implementation of React virtual list

Implementation of React virtual list

1. Background

During the development process, we always encounter the display of many lists. When lists of this magnitude are rendered to the browser, it will eventually cause the browser's performance to degrade. If the amount of data is too large, first of all, the rendering will be extremely slow, and secondly, the page will be stuck directly. Of course, you can choose other ways to avoid it. For example, paging, or downloading files and so on. Here we discuss how to use virtual lists to solve this problem.

2. What is a virtual list

The simplest description: When the list scrolls, change the rendering elements in the visible area.

The [total height of the list] and [height of the visualization area] are calculated through the [estimated height of a single data item]. And render the list as needed within [visualization area height].

3. Introduction to related concepts

The following introduces some very important parameter information in the component. Let's understand it here first to have an impression, so that it will be clearer when you use it later.

  • [Estimated height of a single data item]: The specific height of a specific item in the list. It can be [fixed height] or [dynamic height]
  • [Total height of list]: When all data is rendered, the [total height] of the list
  • [Visualization area height]: The container hanging on the virtual list. The visible area of ​​the list
  • [Estimated number of displayed items]: In [Visualization area height], according to [Estimated height of a single data item], the number of visible data items
  • [Start Index]: [Visualization Area Height] The index of the first data to be displayed
  • [End Index]: [Visual Area Height] The index of the last data item displayed
  • [Each Item Position Cache]: Because the height of the list is not fixed, the height position of each data item is recorded, including index, top, bottom, and lineHeight attributes.

4. Virtual list implementation

The virtual list can be simply understood as: when the list is scrolled, the rendering elements within the [visualization area height] are changed. According to the relevant concepts introduced above, we follow these steps based on these properties:

  • Pass in component data [data list (resources)] and [estimated height (estimatedItemSize)]
  • Calculate the initial position of each piece of data based on [data list (resources)] and [estimated height (estimatedItemSize)] (the placeholder of each piece of data when all are rendered)
  • Calculate the total height of the list
  • [Visual area height] Controlled by CSS
  • According to the [visualization area height], calculate the estimated number of display items in the visualization area
  • Initialize the [header mount element] and [tailer mount element] of the visible window. When scrolling occurs, recalculate the [header mount element] and [tailer mount element] according to the scroll difference and scroll direction.

According to the above introduction steps, let's start to implement a virtual list.

4.1 Driver Development: Parameter Analysis

parameter illustrate type default value
resources Source data array Array []
estimatedItemSize Estimated height of each data number 32px
extrea Used to customize ItemRender and pass other parameters any none
ItemRender Components for rendering each piece of data React.FC const ItemRender = ({ data }: Data) => (<React.Fragment>{String(data) }</React.Fragment>)
key Generates a unique key for the item during traversal. It needs to be a field with a specific unique value in the resources data. Used to improve performance. string Default order customization -> id -> key -> index

4.1.1 ItemRender

import React, { useState } from 'react';
import { VirtualList } from 'biz-web-library';
// Define the component for displaying each piece of data const ItemRender = ({ data }) => {
  let dindex = parseInt(data);
  let lineHeight = dindex % 2 ? '40px' : '80px';
  return (
    <div style={{ lineHeight, background: dindex % 2 ? '#f5f5f5' : '#fff' }}>
      <h3>#{dindex} title name</h3>
      <p>Write whatever you want, no limit on page height</p>
    </div>
  );
};
const ItemRenderMemo = React.memo(ItemRender);

4.1.2 Data List Initialization

// Initialize list data const getDatas = () => {
  const datas = [];
  for (let i = 0; i < 100000; i++) {
    datas.push(`${i} Item`);
  }
  return datas;
};

4.1.3 How to use

// Using virtual list export default () => {
  let [resources, setResources] = useState([]);
  const changeResources = () => {
    setResources(getDatas());
  };

  return (
    <div>
      <button onClick={changeResources}>click me </button>

      <div
        style={{
          height: '400px',
          overflow: 'auto',
          border: '1px solid #f5f5f5',
          padding: '0 10px',
        }}
      >
        <VirtualList
          ItemRender={ItemRenderMemo}
          resources={resources}
          estimatedItemSize={60}
        />
      </div>
    </div>
  );
};

4.2 Component initialization calculation and layout

Now that we know how to use it, let's start implementing our component. According to the passed in data source resources and estimated height estimatedItemSize, calculate the initialization position of each piece of data.

// Overall initialization height of the circular cache list export const initPositinoCache = (
  estimatedItemSize: number = 32,
  length: number = 0,
) => {
  let index = 0,
  positions = Array(length);
  while (index < length) {
    positions[index] = {
      index,
      height: estimatedItemSize,
      top: index * estimatedItemSize,
      bottom: (index++ + 1) * estimatedItemSize,
    };
  }
  return positions;
};

If the height of each data in the list is consistent, then this height will not change. If the height of each piece of data is not fixed, the position will be updated during the scrolling process. Here are some other parameters that need to be initialized:

parameter illustrate type default value
resources Source data array Array []
startOffset The offset of the visible area from the top number 0
listHeight When all data is rendered, the height of the container any none
visibleCount Number of visualization areas on one page number 10
startIndex Visualization area start index number 0
endIndex Visualization area end index number 10
visibleData Data displayed in the visualization area Array []

In fact, for each attribute, its significance can be clearly seen after a brief introduction. However, the [startOffset] parameter needs to be introduced in detail. It is an important property that simulates infinite scrolling during the scrolling process. Its value indicates the position from the top during our scrolling process. [startOffset] achieves the effect of infinite scrolling by combining [visibleData].
Tips: Pay attention to the position of [positions] here, which is equivalent to an external variable of a component. Remember not to hang it on the static property of the component.

// Cache the positions of all items let positions: Array<PositionType>;

class VirtualList extends React.PureComponent{
 
  constructor(props) {
    super(props);
    const { resources } = this.props;

    // Initialize cache positions = initPositinoCache(props.estimatedItemSize, resources.length);
    this.state = {
      resources,
      startOffset: 0,
      listHeight: getListHeight(positions), // bottom attribute of the last data in positions scrollRef: React.createRef(), // virtual list container ref
      items: React.createRef(), // Virtual list display area ref
      visibleCount: 10, // Number of visible areas on a page startIndex: 0, // Start index of visible area endIndex: 10, // // End index of visible area };
  }
  // TODO: Hide some other functionality. . . . .


  //Layout render() {
  const { ItemRender = ItemRenderComponent, extension } = this.props;
  const { listHeight, startOffset, resources, startIndex, endIndex, items, scrollRef } = this.state;
  let visibleData = resources.slice(startIndex, endIndex);

  return (
    <div ref={scrollRef} style={{ height: `${listHeight}px` }}>
      <ul
        ref={items}
        style={{
          transform: `translate3d(0,${startOffset}px,0)`,
        }}
      >
        {visibleData.map((data, index) => {
          return (
            <li key={data.id || data.key || index} data-index={`${startIndex + index}`}>
              <ItemRender data={data} {...extrea}/>
            </li>
          );
        })}
      </ul>
    </div>
  );
  }
} 

4.3 Scrolling triggers registration events and updates

Register onScroll to DOM through [componentDidMount]. In the scrolling event, requestAnimationFrame is used. This method uses the browser's idle time to execute, which can improve the performance of the code. If you want to have a deeper understanding, you can check the specific use of this API.

componentDidMount() {
  events.on(this.getEl(), 'scroll', this.onScroll, false);
  events.on(this.getEl(), 'mousewheel', NOOP, false);

  // Calculate the latest node based on rendering let visibleCount = Math.ceil(this.getEl().offsetHeight / estimatedItemSize);
  if (visibleCount === this.state.visibleCount || visibleCount === 0) {
    return;
  }
  // Update endIndex, listHeight/offset because visibleCount changed this.updateState({ visibleCount, startIndex: this.state.startIndex });
}

getEl = () => {
    let el = this.state.scrollRef || this.state.items;
    let parentEl: any = el.current?.parentElement;
    switch (window.getComputedStyle(parentEl)?.overflowY) {
      case 'auto':
      case 'scroll':
      case 'overlay':
      case 'visible':
        return parentEl;
    }
    return document.body;
};

onScroll = () => {
    requestAnimationFrame(() => {
      let { scrollTop } = this.getEl();
      let startIndex = binarySearch(positions, scrollTop);

      // Because startIndex changes, update endIndex, listHeight/offset this.updateState({ visibleCount: this.state.visibleCount, startIndex});
    });
  };

Next, we analyze the key steps. When scrolling, we can get the [scrollTop] of the current [scrollRef] virtual list container. Through this distance and [positions] (which records all the position properties of each item), we can get the startIndex of that position. To improve performance, we use binary search:

// Tool function, put into tool file export const binarySearch = (list: Array<PositionType>, value: number = 0) => {
  let start: number = 0;
  let end: number = list.length - 1;
  let tempIndex = null;
  while (start <= end) {
    let midIndex = Math.floor((start + end) / 2);
    let midValue = list[midIndex].bottom;

    // If the values ​​are equal, the found node is returned directly (because it is bottom, startIndex should be the next node)
    if (midValue === value) {
      return midIndex + 1;
    }
    // If the middle value is less than the input value, it means that the node corresponding to value is greater than start, and start moves back one position else if (midValue < value) {
      start = midIndex + 1;
    }
    // If the middle value is greater than the input value, it means that value is before the middle value, and the end node moves to mid - 1
    else if (midValue > value) {
      // tempIndex stores all the values ​​closest to value if (tempIndex === null || tempIndex > midIndex) {
        tempIndex = midIndex;
      }
      end = midIndex - 1;
    }
  }
  return tempIndex;
};

Once we get the startIndex, we will update the values ​​of all the properties in the component State based on the startIndex.

 updateState = ({ visibleCount, startIndex }) => {
    // Update data according to the newly calculated node this.setState({
      startOffset: startIndex >= 1 ? positions[startIndex - 1]?.bottom : 0,
      listHeight: getListHeight(positions),
      startIndex,
      visibleCount,
      endIndex: getEndIndex(this.state.resources, startIndex, visibleCount)
    });
  };

// The following is a tool function, placed in other files export const getListHeight = (positions: Array<PositionType>) => {
    let index = positions.length - 1;
    return index < 0 ? 0 : positions[index].bottom;
  };

export const getEndIndex = (
  resources: Array<Data>,
  startIndex: number,
  visibleCount: number,
) => {
  let resourcesLength = resources.length;
  let endIndex = startIndex + visibleCount;
  return resourcesLength > 0 ? Math.min(resourcesLength, endIndex) : endIndex;
}

4.4 Update item heights when they are not equal

At this point, we have completed the basic DOM scrolling, data update and other logic. But during the test, you will find that if the height is not equal, the position and other operations have not been updated? Where to put these?
Here, our [componentDidUpdate] should come in handy. Every time the DOM is rendered, the position and height information of the displayed item should be updated to the [position] attribute. The current total height [istHeight] and offset [startOffset] must also be updated at the same time.

 componentDidUpdate() {
  this.updateHeight();
}

  
updateHeight = () => {
  let items: HTMLCollection = this.state.items.current?.children;
  if (!items.length) return;

  // Update cache updateItemSize(positions, items);

  // Update the total height let listHeight = getListHeight(positions);

  // Update total offset let startOffset = getStartOffset(this.state.startIndex, positions);

  this.setState({
    listHeight,
    startOffset,
  });
};

// The following is a tool function, placed in other files export const updateItemSize = (
  positions: Array<PositionType>,
  items: HTMLCollection,
) => {
  Array.from(items).forEach(item => {
    let index = Number(item.getAttribute('data-index'));
    let { height } = item.getBoundingClientRect();
    let oldHeight = positions[index].height;

    //If there is a difference, update all nodes after this node let dValue = oldHeight - height;
    if (dValue) {
      positions[index].bottom = positions[index].bottom - dValue;
      positions[index].height = height;

      for (let k = index + 1; k < positions.length; k++) {
        positions[k].top = positions[k - 1].bottom;
        positions[k].bottom = positions[k].bottom - dValue;
      }
    }
  });
};

//Get the current offset export const getStartOffset = (
  startIndex: number,
  positions: Array<PositionType> = [],
) => {
  return startIndex >= 1 ? positions[startIndex - 1]?.bottom : 0;
};

export const getListHeight = (positions: Array<PositionType>) => {
  let index = positions.length - 1;
  return index < 0 ? 0 : positions[index].bottom;
};

4.5 External parameter data changes, component data updates

At this last step, if the external data source we passed in has changed, we have to synchronize the data. This operation is of course completed in the getDerivedStateFromProps method.

 static getDerivedStateFromProps(
    nextProps: VirtualListProps,
    prevState: VirtualListState,
  ) {
    const { resources, estimatedItemSize } = nextProps;
    if (resources !== prevState.resources) {
      positions = initPositinoCache(estimatedItemSize, resources.length);

      // Update height let listHeight = getListHeight(positions);

      // Update total offset let startOffset = getStartOffset(prevState.startIndex, positions);

     
      let endIndex = getEndIndex(resources, prevState.startIndex, prevState.visibleCount);
     
      return {
        resources,
        listHeight,
        startOffset,
        endIndex,
      };
    }
    return null;
  }

5 Conclusion

OK, a complete vitural list component is completed. Because the render function of each data ItemRender is customized, you can scroll virtually any item as long as it is in list form. Of course, according to the information I have read online, due to network problems, the scrolling of pictures cannot guarantee the true height of the list items, which may cause inaccuracies. We will not discuss this here for now, and those who are interested can go into more depth.

This is the end of this article about the implementation of React virtual list. For more relevant React virtual list content, please search 123WORDPRESS.COM's previous articles or continue to browse the following related articles. I hope everyone will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • React implements a highly adaptive virtual list

<<:  MySQL 8.0.12 installation and configuration method graphic tutorial (windows10)

>>:  VMware virtualization kvm installation and deployment tutorial summary

Recommend

About Generics of C++ TpeScript Series

Table of contents 1. Template 2. Generics 3. Gene...

Detailed steps for installing and configuring mysql 5.6.21

1. Overview MySQL version: 5.6.21 Download addres...

Detailed explanation of the usage of image tags in HTML

In HTML, the <img> tag is used to define an...

Detailed explanation of CSS3+JS perfect implementation of magnifying glass mode

About a year ago, I wrote an article: Analysis of...

Description of meta viewport attribute in HTML web page

HTML meta viewport attribute description What is ...

Detailed explanation of monitoring NVIDIA GPU usage under Linux

When using TensorFlow for deep learning, insuffic...

Vue gets token to implement token login sample code

The idea of ​​using token for login verification ...

In-depth explanation of closure in JavaScript

Introduction Closure is a very powerful feature i...

How to solve the 2002 error when installing MySQL database on Alibaba Cloud

The following error occurred while installing the...

mysql solves the problem of finding records where two or more fields are NULL

Core code /*-------------------------------- Find...

Detailed explanation of meta tags (the role of meta tags)

No matter how wonderful your personal website is,...

The webpage cannot be opened because the div element lacks a closing tag

At first I thought it was a speed issue, so I late...