0

I am working on a react app where I have table with scroller and on every scroll I am updating the page number and making a subsquent api call with the updated page number but the page number is updating so fast that it exceeds the limit of page number and the api returns empty array and that leads to imcomplete data.

Here's my code:

handleScroll=async ({ scrollTop }) => {
      console.log('hey');
      if (this.props.masterName && this.props.codeSystem) {
        const params = {};
        await this.props.setPageNumber(this.props.page_num + 1);
        params.code_system_category_id = this.props.masterName;
        params.code_systems_id = this.props.codeSystem;
        params.page_num = this.props.page_num;
        if (this.props.entityName) params.entity_name = this.props.entityName;
        if (this.props.status) params.status = this.props.status;
        console.log(params);

        await this.props.fetchCodeSets(params);
      }
    }

This is the function that will get called on every scroll,on every scroll I am incrementing the page number by 1 using await and also making a api call as this.props.fetchCodeSets using await so that scroll doesnt exceed before completing the api call,but the scroll keeps getting called and it leads to the above explained error.

Here's my table with scroll:

<StyledTable
          height={250}
          width={this.props.width}
          headerHeight={headerHeight}
          rowHeight={rowHeight}
          rowRenderer={this.rowRenderer}
          rowCount={this.props.codeSets.length}
          rowGetter={({ index }) => this.props.codeSets[index]}
          LoadingRow={this.props.LoadingRow}
          overscanRowCount={5}
          tabIndex={-1}
          className='ui very basic small single line striped table'
          columnsList={columns}
          onScroll={() => this.handleScroll('scroll')}
        />

I am using react-virtualized table and the docs can be found here: https://github.com/bvaughn/react-virtualized/blob/master/docs/Table.md

Any leads can definitely help!

rudeTool
  • 526
  • 1
  • 11
  • 25
  • What's `setPageNumber`? Can you share the code? What will happen if we set the page number after we get results from the API? Like after the `await this.props.fetchCodeSets(params)` statement. – Sanish Joseph Sep 25 '21 at 16:16
  • setPageNumber will just increment page_number in the redux state by 1. – rudeTool Sep 25 '21 at 16:20
  • What if we make it after the API call? – Sanish Joseph Sep 25 '21 at 16:21
  • still the same behaviour – rudeTool Sep 25 '21 at 16:24
  • just want to know how to control scroll,it should trigger after successfull await calls – rudeTool Sep 25 '21 at 16:24
  • I was trying to say, move ` await this.props.setPageNumber(this.props.page_num + 1);` to bottom of the function so pagenumber won't change until API is done. If the user keeps scrolling this may behave oddly. I am thinking of debouncing the API call. Meaning delay the API call until the user stops scrolling. https://stackoverflow.com/questions/12009367/javascript-event-handling-scroll-event-with-a-delay – Sanish Joseph Sep 25 '21 at 16:30
  • in case I set the page number at the last,it fetches unlimited number of records – rudeTool Sep 25 '21 at 16:36
  • Then I believe you need to debounce the API call. Let the user scroll and stop. Then increment the page by 1. Assuming the scroll ends when they reach the last result. Users will have no option to scroll further so it's safe to call the API now. – Sanish Joseph Sep 25 '21 at 16:42
  • Can you show a simple implementation using any code? – rudeTool Sep 25 '21 at 16:45
  • I will need some time for that. If you can set your code on code sandbox or some similar places, it will be great. – Sanish Joseph Sep 25 '21 at 16:46
  • Actually my code is just a part of a very big redux state env and pasting all that on codesandbox will not be possible – rudeTool Sep 25 '21 at 16:54
  • I will try debouncing by then,ping whenever you get some time – rudeTool Sep 25 '21 at 16:54

1 Answers1

2

You are loading a new page on every scroll interaction. If the user scrolls down by 5 pixels, do you need to load an entire page of data? And then another page when the scrolls down another 2 pixels? No. You only need to load a page when you have reached the end of the available rows.

You could use some math to figure out which pages need to be loaded based on the scrollTop position in the onScroll callback and the rowHeight variable.

But react-virtualized contains an InfiniteLoader component that can handle this for you. It will call a loadMoreRows function with the startIndex and the stopIndex of the rows that you should load. It does not keep track of which rows have already been requested, so you'll probably want to do that yourself.


Here, I am storing the API responses in a dictionary keyed by index, essentially a sparse array, to support any edge cases where the responses come back out of order.

We can check if a row is loaded by seeing if there is data at that index.

We will load subsequent pages when the loadMoreRows function is called by the InfiniteList component.

import { useState } from "react";
import { InfiniteLoader, Table, Column } from "react-virtualized";
import axios from "axios";

const PER_PAGE = 10;

const ROW_HEIGHT = 30;

export default function App() {
  const [postsByIndex, setPostsByIndex] = useState({});
  const [totalPosts, setTotalPosts] = useState(10000);
  const [lastRequestedPage, setLastRequestedPage] = useState(0);

  const loadApiPage = async (pageNumber) => {
    console.log("loading page", pageNumber);
    const startIndex = (pageNumber - 1) * PER_PAGE;
    const response = await axios.get(
      // your API is probably like `/posts/page/${pageNumber}`
      `https://jsonplaceholder.typicode.com/posts?_start=${startIndex}&_end=${
        startIndex + PER_PAGE
      }`
    );
    // This only needs to happen once
    setTotalPosts(parseInt(response.headers["x-total-count"]));
    // Save each post to the correct index
    const posts = response.data;
    const indexedPosts = Object.fromEntries(
      posts.map((post, i) => [startIndex + i, post])
    );
    setPostsByIndex((prevPosts) => ({
      ...prevPosts,
      ...indexedPosts
    }));
  };

  const loadMoreRows = async ({ startIndex, stopIndex }) => {
    // Load pages up to the stopIndex's page. Don't load previously requested.
    const stopPage = Math.floor(stopIndex / PER_PAGE) + 1;
    const pagesToLoad = [];
    for (let i = lastRequestedPage + 1; i <= stopPage; i++) {
      pagesToLoad.push(i);
    }
    setLastRequestedPage(stopPage);
    return Promise.all(pagesToLoad.map(loadApiPage));
  };

  return (
    <InfiniteLoader
      isRowLoaded={(index) => !!postsByIndex[index]}
      loadMoreRows={loadMoreRows}
      rowCount={totalPosts}
      minimumBatchSize={PER_PAGE}
    >
      {({ onRowsRendered, registerChild }) => (
        <Table
          height={500}
          width={300}
          onRowsRendered={onRowsRendered}
          ref={registerChild}
          rowCount={totalPosts}
          rowHeight={ROW_HEIGHT}
          // return empty object if not yet loaded to avoid errors
          rowGetter={({ index }) => postsByIndex[index] || {}}
        >
          <Column label="Title" dataKey="title" width={100} />
          <Column label="Description" dataKey="body" width={200} />
        </Table>
      )}
    </InfiniteLoader>
  );
}

CodeSandbox Link

The placeholder API that I am using takes start and end indexes instead of page numbers, so going back and forth from index to page number to index in this example is silly. But I am assuming that your API uses numbered pages.

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102