Infinite scroll for React List - javascript

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.

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)

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 =)

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)?

Related

React.js + Socket.io updating state changes position of list items

export default class extends React.Component {
constructor(props) {
super(props)
this.state = {
status: [],
services: []
}
getAppData((err,opt, data) => {
function Exists(list, id) {
return list.some(function(el) {
return el.data.id == id;
});
}
if (opt == "sysinfo"){
var filtered = this.state.status;
if (Exists(filtered, data.id)){
filtered = this.state.status.filter(function(el) { return el.data.id != data.id; });
}
filtered.push({ data })
this.setState({status: filtered})
} else if (opt == "init_services"){
this.setState({services: data})
}
});
}
render() {
const timestampforuse = this.state.status
const totalList = this.state.services
console.log(totalList)
const mainList = totalList.map((link) =>
<ListGroup.Item key={link.id} keyProp={link.id}>Name: {link.name} Node: {link.node}</ListGroup.Item>
);
console.log(totalList)
const listItems = timestampforuse.map((link) =>
<ListGroup.Item ><p key={link.data.id}>ID: {link.data.pid} Node: {link.data.node} <br/>Ram usage: {link.data.p_ram.toFixed(2)} / 100% Cpu usage: {link.data.p_cpu.toFixed(2)} / 100%</p></ListGroup.Item>
);
return (
<div>
<ListGroup>
{mainList}
</ListGroup>
</div>
);
}
}
Data from sysinfo:
{
cores: 16,
cpu: 0,
id: "00ffab6ca93243f08eb10670d9c491d54cf674173d13c24a0a663ebb3f5e54d042ae",
node: "1",
p_cpu: 0,
p_ram: 0.18230482881430612,
pid: 29216,
ram: 28.78515625,
threads: 5,
time: 1609179904302,
time_from_startup: 1609179876.271594,
time_since_boot: 1608562209.0201786
}
Data for init:
add_game: true
description: "a test script"
id: "00ffab6ca93243f08eb10670d9c491d54a0a663ebb3f5e54d042ae"
name: "test331112321"
node: "1"
Socket script:
import openSocket from 'socket.io-client';
const socket = openSocket('http://localhost:3000');
function getAppData(cb) {
socket.on('update_system', data => cb(null,"sysinfo", data));
socket.on('init_services', data => cb(null,"init_services", data));
socket.emit('updated', 1000);
}
export { getAppData };
I have tried using a map and using it as a list but when it updates every second it updates too fast to even read. How would I make the name appear, then once data gets sent have that update the list but not update the entire list? At the moment, it allows it to update and change, and no issues if it's 1 item being updated but if there are 2 or more it updates too fast to see. How do I get around this?
I have fixed this by updating an array of objects on the server-side. Updating a service on that list and returning the entire list. This tackled the issue of having it update too fast.
End code front end code:
export default class extends React.Component {
constructor(props) {
super(props)
this.state = {
services: []
}
getAppData((err,opt, data) => {
if (opt == "sysinfo"){
this.setState({services: data})
}
});
}
componentDidMount() {
fetch("http://localhost:3000/api/v1/bot/me/getservices").then(res => res.json()).then(data =>{
console.log(data)
this.setState({services: data})
})
}
render() {
const totalList = this.state.services
const listItems = totalList.map((link) =>
<ListGroup.Item key={link.id}>Name: {link.name} Node: {link.node} <br/>Ram usage: {link.p_ram.toFixed(2)} / 100% Cpu usage: {link.p_cpu.toFixed(2)} / 100%</ListGroup.Item>
);
return (
<div>
<ListGroup>
{listItems}
</ListGroup>
</div>
);
}
}

Executing a loop in React class component

I'm building a pagination component and I'm struggling to execute a for loop so I can dynamically generate the pages. I initially had a function component, but I want to switch it to a class component so I can manage state in it. (I know, I can use hooks, but Im practicing class components at the moment).
I initially added the for loop in the render method but it is executing the loop twice because the component ir rendering twice. Then, I tried componentDidMount() but it doesn't do anything... then used componentWillMount() and it worked. However, I know this could be bad practice.
Any ideas? See below the component with componentDidMount()
import React, { Component } from 'react';
import styles from './Pagination.module.css';
class Pagination extends Component {
state = {
pageNumbers: [],
selected: '',
};
componentDidMount() {
for (
let i = 1;
i <= Math.ceil(this.props.totalDogs / this.props.dogsPerPage);
i++
) {
this.state.pageNumbers.push(i);
}
}
classActiveForPagineHandler = (number) => {
this.setState({ selected: number });
};
render() {
return (
<div className={styles.PaginationContainer}>
<nav>
<ul className={styles.PageListHolder}>
{this.state.pageNumbers.map((num) => (
<li key={num}>
<a
href="!#"
className={
this.state.selected === num
? styles.Active
: styles.PageActive
}
onClick={() => {
this.props.paginate(num);
// this.props.classActiveForPagineHandler(num);
}}
>
{num}
</a>
</li>
))}
</ul>
</nav>
</div>
);
}
}
export default Pagination;
You better push all the numbers into array and then update pageNumbers state. this.state.pageNumbers.push(i); does not update state directly, you need use setState after your calculation completes.
componentDidMount() {
const { pageNumbers = [] } = this.state
const { totalDogs, dogsPerPage } = this.props
for (let i = 1; i <= Math.ceil(totalDogs / dogsPerPage); i++) {
pageNumbers.push(i);
}
this.setState({ pageNumbers })
}
Demo link here
you should not update state like this :
this.state.pageNumbers.push(i);
do this:
this.setState((s) => {
return {
...s,
pageNumbers: [...s.pageNumbers, i]
}
})
Do not mutate state directly in react component. Use setState for all updates.
componentDidMount() {
const pageNumbers = [];
for (
let i = 1;
i <= Math.ceil(this.props.totalDogs / this.props.dogsPerPage);
i++
) {
pageNumbers.push(i);
}
this.setState({ pageNumbers });
}
Alternatively, you can simplify the code using Array.from for this case.
componentDidMount() {
this.setState({
pageNumbers: Array.from(
{ length: Math.ceil(this.props.totalDogs / this.props.dogsPerPage) },
(_, i) => i + 1
),
});
}

How to implement infinite scroll with Github API Repositories List

I'm fetching github repositories from api.github.com/users/ncesar/repos and i wanna get only 10 items, then after scrolling, load more items. I have tried to implement myself but i dont know how to adapt it to array slice(that is limiting my array length to 2, just for testings).
This is my current code
class SearchResult extends Component {
constructor() {
super();
this.state = {
githubRepo: [],
loaded: false,
error: false,
};
}
componentDidMount() {
this.loadItems(this.props.location.state.userName);
}
componentWillReceiveProps(nextProps) {
if (
nextProps.location.state.userName !== this.props.location.state.userName
) {
this.loadItems(nextProps.location.state.userName);
}
}
loadItems(userName) {
axios
.get(`${api.baseUrl}/users/${userName}/repos`)
.then((repo) => {
console.log('repo', repo);
if (repo.data.length <= 0) {
this.setState({ githubRepo: '' });
} else {
this.setState({ githubRepo: repo.data });
}
})
.catch((err) => {
if (err.response.status === 404) {
this.setState({ error: true, loaded: true });
}
});
}
render() {
const {
githubRepo, loaded, error,
} = this.state;
return (
<div className="search-result">
{error === true ? (
<h1 style={style}>User not found :(</h1>
) : (
<section id="user-infos">
<div className="row">
<div className="col-md-8">
{githubRepo
.sort((a, b) => {
if (a.stargazers_count < b.stargazers_count) return 1;
if (a.stargazers_count > b.stargazers_count) return -1;
return 0;
}).slice(0, 2)
.map(name => (
<UserRepositories
key={name.id}
repoName={name.name}
repoDescription={name.description}
starNumber={name.stargazers_count}
/>
))}
</div>
</div>
</section>
)}
</div>
);
}
}
export default SearchResult;
Just to clarify, the sort is ordening repos by stars count.
What i have tried:
//setting theses states and calling this function
page: 1,
totalPages: null,
scrolling: false,
componentDidMount() {
this.loadContacts(); //carrega os contatos iniciais
this.scrollListener = window.addEventListener('scroll', (event) => {//escuta o scroll
this.handleScroll(event);
});
}
handleScroll = () => {
const { scrolling, totalPages, page } = this.state; //pega os 3 pra fora do state
if(scrolling) return; //se ja está scrollando, retorna true
if(totalPages <= page) return; //se o total de páginas é menor ou igual a page
const lastLi = document.querySelector('ul.contacts > li:last-child');//pegando o último li
const lastLiOffset = lastLi.offsetTop + lastLi.clientHeight;
const pageOffset = window.pageYOffset + window.innerHeight;
var bottomOffSet = 20;
if(pageOffset > lastLiOffset - bottomOffSet) this.loadMore();
}
loadMore = () => {
// event.preventDefault();
this.setState(prevState => ({
page: prevState.page + 1,
scrolling: true,
}), this.loadContacts);
}
But i dont know where i can pass the page parameter. The explanation of this code: it was used on a API with page number and per page parameters. The problem is that Github API does not offer this in repositories list, so, this is why i'm using slice.
You can use something like this https://www.npmjs.com/package/react-infinite-scroller
You should create one component (controller) that will download data from github and will provide fetching more function to react-infinite-scroller
And create a other component that will receive and render the data
I have found the solution. I just need to pass a state to slice and increment that state with a function.
This is the code:
{githubRepo
.sort((a, b) => {
if (a.stargazers_count < b.stargazers_count) return 1;
if (a.stargazers_count > b.stargazers_count) return -1;
return 0;
}).slice(0, pageNum)
.map(name => (
<UserRepositories
key={name.id}
repoName={name.name}
repoDescription={name.description}
starNumber={name.stargazers_count}
/>
))}
<button onClick={this.loadMore}>Carregar mais</button>
And loadMore
loadMore = (e) => {
e.preventDefault();
this.setState(prevState => ({
pageNum: prevState.pageNum + 5
}));
}
Now i just need to stop using button and implement to use scroll

React JS - Transform a static pagination to dynamic from JSON

I have a ReactJS code below made for a SO user Piotr Berebecki. The code is working succefully It is a handle pagination returning items from an array. I want to do the same thing but returning data from JSON DB How can I do? How can I solve it? Thank you. Here is the app in the CodePen.
Here is My DB Json. I want only return the images (named as 'fotos'). It will replace the 'todos' in the array in the second code below
. Note: It must be made by Axios.
{
"interiores": [
{
"nome": "house 1",
"descricao": "This is the description of the house 1",
"fotos": [
"int_02", "int_02", "int_02", "int_02", "int_02"
]
}
]
}
Code:
import React, { Component } from 'react'
class Todo extends Component {
constructor() {
super();
this.state = {
todos: ['a','b','c','d','e','f','g','h','i','j','k'],
currentPage: 1,
todosPerPage: 3
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
this.setState({
currentPage: Number(event.target.id)
});
}
render() {
const { todos, currentPage, todosPerPage } = this.state;
// Logic for displaying todos
const indexOfLastTodo = currentPage * todosPerPage;
const indexOfFirstTodo = indexOfLastTodo - todosPerPage;
const currentTodos = todos.slice(indexOfFirstTodo, indexOfLastTodo);
const renderTodos = currentTodos.map((todo, index) => {
return <li key={index}>{todo}</li>;
});
// Logic for displaying page numbers
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(todos.length / todosPerPage); i++) {
pageNumbers.push(i);
}
const renderPageNumbers = pageNumbers.map(number => {
return (
<li
key={number}
id={number}
onClick={this.handleClick}
>
{number}
</li>
);
});
return (
<div>
<ul>
{renderTodos}
</ul>
<ul id="page-numbers">
{renderPageNumbers}
</ul>
</div>
);
}
}
export default Todo;
It seems like you're asking how to handle the data once you've already made the call, this is how I think you should do it using componentDidMount I haven't tested it yet, but should give you a good starting point.
{
"interiores": [
{
"nome": "house 1",
"descricao": "This is the description of the house 1",
"fotos": [
"int_02", "int_02", "int_02", "int_02", "int_02"
]
}
]
}
import React, { Component } from 'react'
class Todo extends Component {
constructor() {
super();
this.state = {
todos: ['a','b','c','d','e','f','g','h','i','j','k'],
currentPage: 1,
todosPerPage: 3 ,
fotos: '',
};
this.handleClick = this.handleClick.bind(this);
}
handleClick(event) {
this.setState({
currentPage: Number(event.target.id)
});
}
async componentDidMount() {
//make call to database and set the db data to your state.
const dbData = axios.get('http://yourapi.com/todods')
.then(function (response) {
this.setState({fotos: response.data.interiores[0].fotos})
})
.catch(function (error) {
console.log('error:', error);
});
}
render() {
const { todos, currentPage, todosPerPage } = this.state;
// Logic for displaying todos
const indexOfLastTodo = currentPage * todosPerPage;
const indexOfFirstTodo = indexOfLastTodo - todosPerPage;
const currentTodos = todos.slice(indexOfFirstTodo, indexOfLastTodo);
const renderTodos = currentTodos.map((todo, index) => {
return <li key={index}>{todo}</li>;
});
// Logic for displaying page numbers
const pageNumbers = [];
for (let i = 1; i <= Math.ceil(todos.length / todosPerPage); i++) {
pageNumbers.push(i);
}
const renderPageNumbers = pageNumbers.map(number => {
return (
<li
key={number}
id={number}
onClick={this.handleClick}
>
{number}
</li>
);
});
return (
<div>
<ul>
{this.state.fotos? this.state.fotos : 'nothing to display' }
</ul>
<ul id="page-numbers">
{renderPageNumbers}
</ul>
</div>
);
}
}
export default Todo;

javascript/ReactJS: Show results from backend in a list

I am sending a GET request on a Node API with a MongoDB server. I am getting the response as JSON in an array of object format. I want to show all those results in a list. Right now i am making a function like this
class VendorDashboard extends React.Component {
constructor() {
super();
this.state = {
paginationValue: '86',
title: ""
}
this.handleLogout = this.handleLogout.bind(this);
this.gotoCourse = this.gotoCourse.bind(this);
}
componentDidMount() {
axios.get('/vendor/showcourses') //the api to hit request
.then((response) => {
console.log(response);
let course = [];
course = response.data.map((courseres) => {
this.setState({
title: courseres.title
});
})
});
Right now what is happening is it is showing just one result. I want to show all results on that api. How can i do it?
This segment here is overriding the title per course.
course = response.data.map((courseres) => {
this.setState({
title: courseres.title
});
})
You can keep the state as an array of titles and do;
course = response.data.map((courseres) => {
return courseres.title;
})
this.setState({titles: course});
And then you can repeat on the array of titles in your component.
Like so in the render method;
const { titles } = this.state;
return <div>{titles.map((title, index) => <div key={index}>{title}</div>)}</div>
You need to collect all the server response and set that as an array of data to the state and use this state data to render:
class VendorDashboard extends React.Component {
constructor() {
super();
this.state = {
paginationValue: '86',
course: []
}
this.handleLogout = this.handleLogout.bind(this);
this.gotoCourse = this.gotoCourse.bind(this);
}
componentDidMount() {
axios.get('/vendor/showcourses') //the api to hit request
.then((response) => {
const course = response.data.map((courseres) => ({
id: courseres.id,
title: courseres.title
}));
this.setState({
course
});
});
}
render() {
return (
<ul>
{
this.state.course.map((eachCourse) => {
return <li key={eachCourse.id}>{eachCourse.title}</li>
})
}
</ul>
)
}
}
In each map iteration you rewrite your piece of state, it is wrong.
Just put courses in your state:
console.log(response);
this.setState({ courses: response.data });
In render method go through your state.courses:
render(){
return(
<div>
{this.state.courses.map(course => <h2>{course.title}</h2>)}
</div>
);
}

Categories

Resources