4

So I have a list of 5k elements. I want to display them in parts, say each part is 30 items. The list of items is in the component's state. Each item is an object taken from the API. It has properties on which I have to make an API call. By parts, to avoid enormous load time. So this is what I've got so far(simplified):

let page=1;

class GitHubLists extends Component {
    constructor(props) {
        super(props);

        this.state = {
            repos: [],
            contributors: []
        }
    }
    componentDidMount() {
      window.addEventListener('scroll', this.handleScroll);
      axios.get(org)
        .then(res => setState({contributors: res})
    }
     handleScroll() {
        page++;
    }
    componentWillUnmount() {
        window.removeEventListener('scroll', this.handleScroll);
    }
    render() {
        const contributors = this.state.contributors.slice(0,30*page).map(contributor =>
            <li key={contributor.id}>{contributor.login} {contributor.contributions}<a href={contributor.url}>View on GitHub</a></li>
        );
        return (
            <div onScroll={this.handleScroll}>{contributors}</div>
        )
    }
}

Like I said each item(contributor in this case) has properties which values are links for the API calls. 3 to be exact. On each one of them, I need to make an API call, count the items inside the response and display them.

3
  • I cannot understand exactly what you really want, your code looks fine for me. Do you want to paginate your response or something like that? Commented Aug 19, 2017 at 15:18
  • Kind of, there are 5k elements and I want to display them 30 times at a time, loading dynamically further content and appending it to the list. Commented Aug 19, 2017 at 15:21
  • The more performatic way of doing this was paginating your API, but sometimes it's not possible, I will try to elaborate a nice question for you about this. Commented Aug 19, 2017 at 15:22

3 Answers 3

2

You can use react-virtualized (6.8k stars), it has been designed for this purpose.

Here is an official example with a list of 1000 elements or here with a Infinite Loader.

I wrote an easier live example here where you can modify code.

For your problem, you need to do your API calls in the rowRenderer and play with the overscanRowCount to prefetch rows. (docs of the List component)

Sign up to request clarification or add additional context in comments.

Comments

0

I've made a simple pagination adapted from another GIST that I've already used that makes total sense for your purpose, you just need to implement your code.

class ItemsApp extends React.Component {
  constructor() {
    super();
    this.state = {
      items: ['a','b','c','d','e','f','g','h','i','j','k','2','4','1','343','34','a','b','c','d','e','f','g','h','i','j','k','2','4','1','343','34','a','b','c','d','e','f','g','h','i','j','k','2','4','1','343','34','33'],
      currentPage: 1,
      itemsPerPage: 30
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(event) {
    this.setState({
      currentPage: Number(event.target.id)
    });
  }

  render() {
    const { items, currentPage, itemsPerPage } = this.state;

    // Logic for displaying current items
    const indexOfLastItem = currentPage * itemsPerPage;
    const indexOfFirstItem = indexOfLastItem - itemsPerPage;
    const currentItems = items.slice(indexOfFirstItem, indexOfLastItem);

    const renderItems = currentItems.map((item, index) => {
      return <li key={index}>{item}</li>;
    });

    // Logic for displaying page numbers
    const pageNumbers = [];
    for (let i = 1; i <= Math.ceil(items.length / itemsPerPage); i++) {
      pageNumbers.push(i);
    }

    const renderPageNumbers = pageNumbers.map(number => {
      return (
        <li
          key={number}
          id={number}
          onClick={this.handleClick}
        >
          {number}
        </li>
      );
    });

    return (
      <div>
        <ul>
          {renderItems}
        </ul>
        <ul id="page-numbers">
          {renderPageNumbers}
        </ul>
      </div>
    );
  }
}


ReactDOM.render(
  <ItemsApp />,
  document.getElementById('app')
);

https://codepen.io/anon/pen/jLZjQZ?editors=0110

Basically, you should insert your fetched array inside the items state, and change the itemsPerPage value according to your needs, I've set 30 occurrences per page.

I hope it helps =)

5 Comments

It helps somehow, but now, how to start the render method after API data has been fetched? Now it maps through empty array
you can create a state isLoaded: false, and set to true after your axios response like this => setState({contributors: res, isLoaded: true});
After that, wrap your return render inside an isLoaded ternary for e.g. { this.state.isLoaded ? <div onScroll={this.handleScroll}>{contributors}</div> : null }
Try to organize these tips and reproduce, if you cannot solve I will bring the solution for you, right now I'm AFK
0

Ok, there is definitely something wrong about how I wrote my app. It is not waiting for all API calls to finish. It sets the state (and pushes to contributors) multiple times. This is the full code:

let unorderedContributors = [];
let contributors = [];

class GitHubLists extends Component {
    constructor(props) {
        super(props);

        this.state = {
            repos: [],
            contributors: [],
            currentPage: 1,
            itemsPerPage: 30,
            isLoaded: false
        };
        this.handleClick = this.handleClick.bind(this)
    }
    componentWillMount() {
    //get github organization
        axios.get(GitHubOrganization)
        .then(res => {
            let numberRepos = res.data.public_repos;
            let pages = Math.ceil(numberRepos/100);
            for(let page = 1; page <= pages; page++) {
            //get all repos of the organization
                axios.get(`https://api.github.com/orgs/angular/repos?page=${page}&per_page=100&${API_KEY}`)
                .then(res => {
                    for(let i = 0; i < res.data.length; i++) {
                        this.setState((prevState) => ({
                            repos: prevState.repos.concat([res.data[i]])
                          }));
                    }
                })
                .then(() => {
                //get all contributors for each repo
                    this.state.repos.map(repo =>
                    axios.get(`${repo.contributors_url}?per_page=100&${API_KEY}`)
                    .then(res => {
                        if(!res.headers.link) {
                            unorderedContributors.push(res.data);
                        }
                            //if there are more pages, paginate through them
                        else {
                            for(let page = 1; page <= 5; page++) { //5 pages because of GitHub restrictions - can be done recursively checking if res.headers.link.includes('rel="next"')
                            axios.get(`${repo.contributors_url}?page=${page}&per_page=100&${API_KEY}`)
                            .then(res => unorderedContributors.push(res.data));
                            }
                        }
                    })
                    //make new sorted array with useful data
                    .then(() => {contributors = 
                        _.chain(unorderedContributors)
                        .flattenDeep()
                        .groupBy('id')
                        .map((group, id) => ({
                            id: parseInt(id, 10),
                            login: _.first(group).login,
                            contributions: _.sumBy(group, 'contributions'),
                            followers_url: _.first(group).followers_url,
                            repos_url: _.first(group).repos_url,
                            gists_url: _.first(group).gists_url,
                            avatar: _.first(group).avatar_url,
                            url: _.first(group).html_url
                          }))
                        .orderBy(['contributions'],['desc'])
                        .filter((item) => !isNaN(item.id))
                        .value()})
                    .then(() => 
                                this.setState({contributors, isLoaded: true})
                        )
                    )
                })
            }
        })          
    }
    handleClick(event) {
        this.setState({currentPage: Number(event.target.id)})
    }
    render() {
        const { contributors, currentPage, contributorsPerPage } = this.state;


        //Logic for displaying current contributors
        const indexOfLastContributor = currentPage * contributorsPerPage;
        const indexOfFirstContributor = indexOfLastContributor - contributorsPerPage;
        const currentContributors = contributors.slice(indexOfFirstContributor, indexOfLastContributor);

        const renderContributors = currentContributors.map((contributor, index) => {
            return <li key={index}>{contributor}</li>;
        });

        //Logic for displaying page numbers
        const pageNumbers = [];
        for (let i = 1; i <= Math.ceil(contributors.length / contributorsPerPage); i++) {
            pageNumbers.push(i);
        }

        const renderPageNumbers = pageNumbers.map(number => {
            return (
                <li 
                key={number}
                id={number}
                onClick={this.handleClick}
                >
                {number}
                </li>
            );
        });

        return (
            <div>
                <ul>
                    {renderContributors}
                </ul>
                <ul id="page-numbers">
                    {renderPageNumbers}
                </ul>
            </div>
        );

    }
}

How can I fix it so the state is set once and then I can render contributors from the state (and making API calls with values of the properties: followers_url, repos_url and gists_url)?

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.