react-vertualize, scrollbar goes top directly when there is less data

16 views Asked by At

description with example: I am having 24 rows in database which are getting from getContractReviewListQuery?.data. initally i am calling api twice so i am getting 18 record (each time i am getting 9 records) when i am scrolling down, i am getting next 6 records from api and scrollbar start flikering at one postion later goes down. how to stop flickering this issue does not come when you have more data, its occurs for above condition where data is less. Can anyone give solution on this?

below how i use my List component and rest of code

        <List
          ref={listRef}
          rows={getContractReviewListQuery?.data || []}
          renderRow={renderRequestRow}
          renderSkeletonRow={renderSkeletonRow}
          hasMore={isHasMorePages}
          loadMore={handleLoadMore}
          isLoading={getContractReviewListQuery?.isPending}
          loadingRowCount={loadingRowCount}
          isLoadMoreDirty={!getContractReviewListQuery?.isIdle}
          renderNoRows={() => <EmptyListMessage title={MESSAGES.NO_RECORDS_FOUND} message={MESSAGES.KINDLY_REFINE_THE_SEARCH_CRITERIA} className="mt-6" />}
        />

list component

import { forwardRef } from "react";
import { InfiniteLoader, List as VirtualizeList, AutoSizer } from "react-virtualized";
import cx from "../../utils/class-names";
import PropTypes from "prop-types";
import useList from "./useList";

const List = forwardRef(function List(props, listRef) {
  const {
    isRenderNoRows,
    renderNoRows,
    getInfiniteLoaderProps,
    getListProps,
    classNames = {},
  } = useList({
    ...props,
    listRef,
  });

  if (isRenderNoRows) {
    return <div className={cx("list_no-row w-full flex m-4 bg-[#F4F9FF] items-center justify-center", classNames.renderNoRows)}>{renderNoRows()}</div>;
  }

  return (
    <AutoSizer className="list auto-sizer">
      {({ width, height }) => (
        <InfiniteLoader {...getInfiniteLoaderProps()} className="infinite-loader">
          {({ onRowsRendered, registerChild }) => (
            <VirtualizeList
              width={width}
              height={height}
              ref={(ref) => {
                if (listRef) listRef.current = ref;
                registerChild(ref);
              }}
              className="virtualize-list"
              onRowsRendered={onRowsRendered}
              {...getListProps()}
            />
          )}
        </InfiniteLoader>
      )}
    </AutoSizer>
  );
});

List.defaultProps = {
  rows: [],
  renderRow: null,
  renderSkeletonRow: null,
  isLoading: false,
  isLoadMoreDirty: false,
  loadingRowCount: 0,
  hasMore: false,
};

List.propTypes = {
  rows: PropTypes.array,
  renderRow: PropTypes.func,
  renderSkeletonRow: PropTypes.func,
  isLoading: PropTypes.bool,
  isLoadMoreDirty: PropTypes.bool,
  loadingRowCount: PropTypes.number,
  hasMore: PropTypes.bool,
};

export default List;

below is useList hook

import { CellMeasurerCache } from "react-virtualized";
import { useCallback, useMemo, useEffect, useState } from "react";

import useRows from "./useRows";
import debounce from "../../utils/debounce";
import useWindowSize from "../../hooks/use-window-size";
import usePrevious from "../../hooks/use-previous";
import is from "../../utils/is";
import useForceUpdate from "../../hooks/use-force-update";
import Row from "./row";

export default function useList(props) {
    console.log('props: ', props);
  const {
    rows: _rows = [],
    renderRow,
    renderSkeletonRow,
    defaultHeight,
    loadMore: _loadMore,
    hasMore,
    threshold = 1,
    isLoading = false,
    loadingRowCount = 1,
    renderNoRows = () => null,
    isLoadMoreDirty,
    classNames,
    listRef,
  } = props;

  const forceUpdate = useForceUpdate();
  const { rows, rowCount, prevRowCount } = useRows(_rows, loadingRowCount);
  const [scrollTop, setScrollTop] = useState(0);
  const size = useWindowSize();
  const previousSize = usePrevious(size);
  const cache = useMemo(
    () =>
      new CellMeasurerCache({
        defaultHeight: defaultHeight,
        fixedWidth: true,
      }),
    [defaultHeight]
  );

  const isRowLoaded = useCallback(
    (index) => {
      return index < rowCount;
    },
    [rowCount]
  );

  const rowRenderer = useCallback(
    ({ index, key, style, parent }) => {
      let rowToRender;
      let row = rows[index];

      let isSkeletonRow = row.isSkeletonRow;
      if (isSkeletonRow) {
        rowToRender = renderSkeletonRow && renderSkeletonRow({ row, index, key, style: {}, rowCount });
      } else if (renderRow) {
        rowToRender = renderRow && renderRow({ row, index, key, style: {}, rowCount });
      }

      if (rowToRender === undefined) return null;

      return (
        <Row cache={cache} key={key} rowKey={key} parent={parent} rowIndex={index} style={style} listRef={listRef}>
          {rowToRender}
        </Row>
      );
    },
    [cache, listRef, renderRow, renderSkeletonRow, rowCount, rows]
  );

  const loadMore = debounce(function (pageNumber) {
    _loadMore && _loadMore(pageNumber);
  }, 150);

  const loadMoreRows = useCallback(
    ({ stopIndex }) => {
      if (stopIndex + 1 === rowCount && hasMore && !isLoading) {
        loadMore(Number(stopIndex) + 1);
      }
    },
    [rowCount, hasMore, isLoading, loadMore]
  );

  const scrollToTop = useCallback(() => {
    setScrollTop(0);
  }, []);

  const handleListScroll = useCallback(({ scrollToTop }) => {
    setScrollTop(scrollToTop);
  }, []);

  const isRenderNoRows = isLoadMoreDirty && !isLoading && rowCount - loadingRowCount === 0;

  const getInfiniteLoaderProps = useCallback(() => {
    return {
      rowCount,
      isRowLoaded,
      loadMoreRows,
      threshold,
    };
  }, [isRowLoaded, loadMoreRows, rowCount, threshold]);

  const getListProps = useCallback(() => {
    return {
      rowCount,
      rowHeight: cache.rowHeight,
      rowRenderer,
      deferredMeasurementCache: cache,
      scrollTop,
      onScroll: handleListScroll,
      scrollToTop,
    };
  }, [cache, handleListScroll, rowCount, rowRenderer, scrollToTop, scrollTop]);

  const clearCache = useCallback(
    (clearCacheStartIndex = 0) => {
      if (is.undefined(clearCacheStartIndex) && clearCacheStartIndex < 0) return;
      if (clearCacheStartIndex < 10) {
        clearCacheStartIndex = 0;
      }
      for (let i = clearCacheStartIndex; i < rowCount; i++) {
        cache.clear(i);
      }
      listRef?.current?.forceUpdateGrids && listRef?.current?.forceUpdateGrids();
      listRef?.current?.recomputeGridSize && listRef?.current?.recomputeGridSize();
      forceUpdate();
    },
    [cache, forceUpdate, listRef, rowCount]
  );

  useEffect(() => {
    // NOTE: check previous width and current width based on that clear cache
    let clearCacheStartIndex;

    if (previousSize?.width !== size?.width) {
      clearCacheStartIndex = 0;
    } else {
      clearCacheStartIndex = prevRowCount - loadingRowCount;
    }
    clearCache(clearCacheStartIndex);
  }, [clearCache, loadingRowCount, prevRowCount, previousSize?.width, size?.width]);

  useEffect(() => {
    let listRefCurrent = listRef?.current;
    if (listRefCurrent) {
      listRefCurrent.clearCache = clearCache;
    }
    return () => {
      if (listRefCurrent) {
        listRefCurrent.clearCache = () => {};
      }
    };
  }, [clearCache, listRef]);

  return {
    isRenderNoRows,
    renderNoRows,
    getInfiniteLoaderProps,
    getListProps,
    classNames,
  };
}

below is useRow hook

import { useRef, useEffect, useState } from "react";

export default function useRows(data = [], loadingRowCount) {
  const [rows, setRows] = useState({});
  const rowRef = useRef({});

  useEffect(() => {
    rowRef.current = rows;
    let _rows = {};
    if (data.length > 0) {
      _rows = data.reduce((prev, current, currentIndex) => {
        prev[currentIndex] = current;
        return prev;
      }, {});
    }

    //NOTE:added to show skeleton _rows
    if (loadingRowCount > 0) {
      let rowLength = Object.keys(_rows).length;
      _rows = Array.from({ length: loadingRowCount }).reduce((prev, current, currentIndex) => {
        prev[rowLength + currentIndex] = { isSkeletonRow: true };
        return prev;
      }, _rows);
    }

    setRows(_rows);
    //NOTE: dont include 'rows' as a dependency or it will go in ifinite loop
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [data, loadingRowCount]);

  const rowCount = Object.keys(rows).length;
  const prevRows = rowRef.current;
  const prevRowCount = Object.keys(rowRef.current).length;

  return {
    rows,
    rowCount,
    prevRows,
    prevRowCount,
  };
}

below is row componet

import { useEffect } from "react";
import { CellMeasurer } from "react-virtualized";
import useResizeObserver from "use-resize-observer";
import usePrevious from "../../hooks/use-previous";

export default function Row(props) {
  let { children, style, rowIndex, cache, rowKey, parent, listRef } = props;
  const { height, ref } = useResizeObserver();
  const prevHeight = usePrevious(height);

  useEffect(() => {
    if (prevHeight !== height) {
      listRef?.current?.clearCache && listRef?.current?.clearCache(rowIndex);
    }
  }, [height, listRef, prevHeight, rowIndex]);
  return (
    <CellMeasurer cache={cache} columnIndex={0} key={rowKey} parent={parent} rowIndex={rowIndex}>
      <div style={style} className="style-element">
        <div ref={ref} className="observer-element">
          {children}
        </div>
      </div>
    </CellMeasurer>
  );
}

debounce function used in useList

export default function debounce(func, wait, immediate) {
  let timeout;
  return function () {
    const context = this;
    const args = arguments;
    const later = function () {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };
    const callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) func.apply(context, args);
  };
}

useWindowSize component

import { useState, useEffect } from "react";

function useWindowSize() {
  const [windowSize, setWindowSize] = useState({
    width: undefined,
    height: undefined,
  });

  useEffect(() => {
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    };
    window.addEventListener("resize", handleResize);
    handleResize();
    return () => window.removeEventListener("resize", handleResize);
  }, []);
  return windowSize;
}

export default useWindowSize;

usePrevious hook

import { useEffect, useRef } from "react";

export default function usePrevious(value) {
  const ref = useRef();
  useEffect(() => {
    ref.current = value;
  }, [value]);
  return ref.current;
}

is function

const TYPES = {
  UNDEFINED: "[object Undefined]",
  NULL: "[object Null]",
  BOOLEAN: "[object Boolean]",
  NUMBER: "[object Number]",
  STRING: "[object String]",
  ARRAY: "[object Array]",
  OBJECT: "[object Object]",
  FUNCTION: "[object Function]",
};
function getType(val) {
  return Object.prototype.toString.call(val);
}
const is = {
  undefined: (val) => getType(val) === TYPES.UNDEFINED,
  null: (val) => getType(val) === TYPES.NULL,
  boolean: (val) => getType(val) === TYPES.BOOLEAN,
  number: (val) => getType(val) === TYPES.NUMBER,
  string: (val) => getType(val) === TYPES.STRING,
  array: (val) => getType(val) === TYPES.ARRAY,
  object: (val) => getType(val) === TYPES.OBJECT,
  function: (val) => getType(val) === TYPES.FUNCTION,
};
export default is;

useForceUpdate

import { useCallback, useState } from "react";

// Returning a new object reference guarantees that a before-and-after
//   equivalence check will always be false, resulting in a re-render, even
//   when multiple calls to forceUpdate are batched.

export default function useForceUpdate() {
  const [, dispatch] = useState(null);

  // Turn dispatch(required_parameter) into dispatch().
  const memoizedDispatch = useCallback(() => {
    dispatch(null);
  }, [dispatch]);
  return memoizedDispatch;
}

description with example: I am having 24 rows in database which are getting from getContractReviewListQuery?.data. initally i am calling api twice so i am getting 18 record (each time i am getting 9 records) when i am scrolling down, i am getting next 6 records from api and my scroll starts flikering at one postion then goes down. how to stop flickering this issue does not come when you have more data, its occurs for above condition where data is less. Can anyone give solution on this?

  • api data is working fine. all funtions passed to List props are also fine
0

There are 0 answers