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!
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.
Related
I'm trying to make a page that gets picture from a server and once all pictures are downloaded display them, but for some reason the page doesn't re-render when I update the state.
I've seen the other answers to this question that you have to pass a fresh array to the setImages function and not an updated version of the previous array, I'm doing that but it still doesn't work.
(the interesting thing is that if I put a console.log in an useEffect it does log the text when the array is re-rendered, but the page does not show the updated information)
If anyone can help out would be greatly appreciated!
Here is my code.
export function Profile() {
const user = JSON.parse(window.localStorage.getItem("user"));
const [imgs, setImages] = useState([]);
const [num, setNum] = useState(0);
const [finish, setFinish] = useState(false);
const getImages = async () => {
if (finish) return;
let imgarr = [];
let temp = num;
let filename = "";
let local = false;
while(temp < num+30) {
fetch("/get-my-images?id=" + user.id + "&logged=" + user.loggonToken + "&num=" + temp)
.then(response => {
if(response.status !== 200) {
setFinish(true);
temp = num+30;
local = true;
}
filename = response.headers.get("File-Name");
return response.blob()
})
.then(function(imageBlob) {
if(local) return;
const imageObjectURL = URL.createObjectURL(imageBlob);
imgarr[temp - num] = <img name={filename} alt="shot" className="img" src={imageObjectURL} key={temp} />
temp++;
});
}
setNum(temp)
setImages(prev => [...prev, ...imgarr]);
}
async function handleClick() {
await getImages();
}
return (
<div>
<div className="img-container">
{imgs.map(i => {
return (
i.props.name && <div className="img-card">
<div className="img-tag-container" onClick={(e) => handleView(i.props.name)}>{i}</div>
<div className="img-info">
<h3 className="title" onClick={() => handleView(i.props.name)}>{i.props.name.substr(i.props.name.lastIndexOf("\\")+1)}<span>{i.props.isFlagged ? "Flagged" : ""}</span></h3>
</div>
</div>
)
})}
</div>
<div className="btn-container"><button className="load-btn" disabled={finish} onClick={handleClick}>{imgs.length === 0 ? "Load Images" : "Load More"}</button></div>
</div>
)
}
I think your method of creating the new array is correct. You are passing an updater callback to the useState() updater function which returns a concatenation of the previous images and the new images, which should return a fresh array.
When using collection-based state variables, I highly recommend setting the key property of rendered children. Have you tried assigning a unique key to <div className="img-card">?. It appears that i.props.name is unique enough to work as a key.
Keys are how React associates individual items in a collection to their corresponding rendered DOM elements. They are especially important if you modify that collection. Whenever there's an issue with rendering collections, I always make sure the keys are valid and unique. Even if adding a key doesn't fix your issue, I would still highly recommend keeping it for performance reasons.
It is related to Array characteristics of javascript.
And the reason of the console log is related with console log print moment.
So it should be shown later updated for you.
There are several approaches.
const getImages = async () => {
... ...
setNum(temp)
const newImage = [...prev, ...imgarr];
setImages(prev => newImage);
}
const getImages = async () => {
... ...
setNum(temp)
setImages(prev => JOSN.parse(JSON.object([...prev, ...imgarr]);
}
const getImages = async () => {
... ...
setNum(temp)
setImages(prev => [...prev, ...imgarr].slice(0));
}
Maybe it could work.
Hope it will be helpful for you.
Ok the problem for me was the server was not sending a proper filename header so it was always null so the condition i.props.name was never true... lol sorry for the confusion.
So the moral of this story is, always make sure that it's not something else in your code that causes the bad behavior before starting to look for other solutions...
I am using Firestore database to store my crypto trades. Since there are a lot of them, I have to load them using the .limit(numberOfTrades) query.
My query: const tradesRef = firebase.firestore().collection("trades").limit(15);
Inside my useEffect:
useEffect(() => {
tradesRef
.where("type", "==", "fiveMinutes")
.get()
.then((collections) => {
const tradesData = collections.docs.map((trade) => trade.data());
const lastDoc = collections.docs[collections.docs.length - 1];
setTrades(tradesData);
setLastTrades(lastDoc);
});
setDataLoading(false);
}, [filter]);
However, I do need pagination in order to load the next set of trades. The pagination of next is already implemented and fairly simple. This is the function I am using for next page:
const fetchMore = () => {
tradesRef
.startAfter(lastTrades)
.get()
.then((collections) => {
const tradesData = collections.docs.map((trade) => trade.data());
const lastDoc = collections.docs[collections.docs.length - 1];
setTrades(tradesData);
setLastTrades(lastDoc);
});
}
Now I am trying to figure out how to implement a previous page query that gets the previous 12 trades. I have researched and implemented a few queries but I am not even close to solving the issue. Any help would be appreciated.
If you want to paginate backwards, you need to use a combination of:
.endBefore(firstItemOnNextPage)
.limitToLast(12)
So firstItemOnNextPage is the first item on page N+1, where you're paginating backwards from. And you're then asking for the last 12 items before that anchor item.
I wanted to create pagination in React. All data comes from store. In this code I wanted to implement search engine. On this time I don't have this but i wrote search method which simulate that. OK, it works but - after I click hello, it display only items from category 2 but it display all the time this same pages (in my case 3). If I click 2 times more, it display only 1 page. I added setCountItems and setPages into search becouse this hooks doesn't update automaticlly.
import React, { useEffect, useState, useRef } from 'react'
import { connect} from 'react-redux'
import Article from './article'
const ArticlesContainer = ({ articles }) => {
const [allItems, setAllItems] = useState(articles.list);
const [pageNumber, setPageNumber] = useState(1);
const [perSite, setPerSite] = useState(10);
const [totalItems, setCountItems] = useState(allItems.length);
const from = (pageNumber - 1) * perSite;
const to = ((pageNumber - 1) * perSite) + perSite;
const [pages, setPages] = useState(Math.ceil(totalItems / perSite));
const handlePageClick = (i) => {
setPageNumber(i);
}
const search = () => {
setAllItems(allItems.filter(x => x.category=== 2 ));
setCountItems(allItems.length);
setPages(Math.ceil(totalItems / perSite));
}
const Pagination = ({pages}) => {
let list = []
for(let i = 1; i<=pages; i++){
list.push(<li key={i} onClick={() => handlePageClick(i)}>{i}</li>)
}
return list;
}
return (
<React.Fragment>
<a onClick={search}>Hello</a>
{allItems.slice(from, to).map(article =>
<Article key={article.id} article={article} />
)}
<div className="row">
<ul>
<Pagination pages={pages} />
</ul>
</div>
</React.Fragment>
);
}
const mapStateToProps = state => ({
articles: state.articles
})
export default connect(mapStateToProps, null)(ArticlesContainer);
Where problem is?
You seem to be suffering from a common misunderstanding of how State works in React. Updating state, whether via this.setState in class or via the "update function" returned by the useState hook, doesn't "automagically" change the relevant state value then and there. In class components, that's because of how React's implementation of setState works (it's asynchronous), but with Hooks it should be perfectly obvious if you stop to think about it. setAllItems is a function, while allItems is an array - and they don't have anything directly to do each other. Calling setAllItems doesn't change the value of allItems - because how could it? allItems is just a variable, the only way to give it a new value is to directly mutate or reassign it - clearly calling a separate function, setAllItems, with an argument that isn't allItems, can't possibly do that.
What it instead does is schedule a rerender of the component - that is, schedules a subsequent call of your function that represents the component - and ensures that the useState call corresponding to allItems will then return value you set. But this is necessarily a rather indirect process. In particular, allItems will have the value you want on the next render of your component, but that search function won't be called (until the user clicks the button again), so the setCountItems(allItems.length); call won't automatically trigger with the "correct" length (the updated length after filtering).
In your case the solution to the problem is very simple. You've overcomplicated your component by introducing far too many state variables, most of which are dependent on each other. Instead of const [totalItems, setCountItems] = useState(allItems.length);, just put const totalItems = allItems.length; - then this will automatically be recalculated to the correct value on every render. You've no need of a setCountItems function, as you know that it will always be equal to allItems.length - it doesn't vary independently.
Similarly, you can vastly simplify much else in this component, since the only things which can vary independently, and therefore which needs to be part of state, are the article list and the page number. This is how I would rewrite your component:
const perSite = 10;
const ArticlesContainer = ({ articles }) => {
const [allItems, setAllItems] = useState(articles.list);
const [pageNumber, setPageNumber] = useState(1);
const totalItems = allItems.length;
const from = (pageNumber - 1) * perSite;
const to = ((pageNumber - 1) * perSite) + perSite;
const pages = Math.ceil(totalItems / perSite);
const handlePageClick = (i) => {
setPageNumber(i);
}
const search = () => {
setAllItems(allItems.filter(x => x.category=== 2 ));
}
const Pagination = ({pages}) => {
let list = []
for(let i = 1; i<=pages; i++){
list.push(<li key={i} onClick={() => handlePageClick(i)}>{i}</li>)
}
return list;
}
return (
<React.Fragment>
<a onClick={search}>Hello</a>
{allItems.slice(from, to).map(article =>
<Article key={article.id} article={article} />
)}
<div className="row">
<ul>
<Pagination pages={pages} />
</ul>
</div>
</React.Fragment>
);
}
I'm trying react hooks and made my first component. Everything is working fine except a flickering problem wile data is loading, here's my component (a simple list of articles, with prev/next pagination) :
function InfoFeed ({ limit, uuid }) {
const [currentIndex, setCurrentIndex] = useState(1)
const { data, loading } = useQuery(gql(queries.InfoFeed), {
variables: {
offset: (articlesByPage * (currentIndex - 1))
}
})
const { articles } = data
const incrementCb = () => setCurrentIndex(currentIndex === maxPaginationIndex ? 1 : currentIndex + 1)
const decrementCb = () => setCurrentIndex(currentIndex === 1 ? maxPaginationIndex : currentIndex - 1)
return (
<Panel title="Fil Info">
<StyledList>
{articles && articles.map((article, index) => (<InfoItem key={index} article={article}/>))}
</StyledList>
<button onClick={incrementCb}>next</button>
<button onClick={decrementCb}>prev</button>
</Panel>
)
}
The problem is : when clicking on prev/next buttons, it launches a server request, loading is automatically set to true, and data is set to undefined. Data being undefined, the article list is empty while loading. I could show a spinner conditionnaly using the loading variable, but it's a pretty fast request (no spinner required), and it wouldn't prevent the previous data from disappearing, which is the cause of this unwanted flickering.
Any idea how to deal with this ? Thanks !
Solved by migrating to official #apollo/react-hooks bindings, since I was using https://github.com/trojanowski/react-apollo-hooks
DISCLAIMER: I have not used React hooks much, or Apollo at all. This answer is speculative.
From looking at the documentation for #apollo/react-hooks, I would try setting the fetchPolicy for your query to network-only. That way, it shouldn't return the initial data from the cache (which is empty) first.
const { data, loading } = useQuery(gql(queries.InfoFeed), {
variables: {
offset: (articlesByPage * (currentIndex - 1))
},
fetchPolicy: 'network-only'
});
I'm building an infinite scroller in cycle.js. I have an ajax service that returns X results per page. The first request is with a page id of 0, but subsequent requests need to use the page id returned in the first result set. The results are placed in the dom. When the user scrolls to the bottom of the visible results, a new set is loaded and concatenated to the list. I have something that works, but not as well as I would like.
The shortened version looks like:
const onScroll$ = sources.DOM.select('document').events('scroll')
.map((e) => {
return e.srcElement.scrollingElement.scrollTop;
})
.compose(debounce(1000));
const onHeight$ = sources.DOM.select('#items').elements()
.map((e) => {
return e[0].scrollHeight;
})
const scroll$ = onScroll$.startWith(0);
const height$ = onHeight$.startWith(0);
const itemsXHR$ = sources.HTTP.select('items')
.flatten();
const id$ = itemsXHR$
.map(res => res.body.data.content.listPageId)
.take(2)
.startWith(0);
const getitems$ = xs.combine(scroll$,height$,id$)
.map( ( [scrollHeight, contentHeight, sid, lp] ) => {
if ( scrollHeight > (contentHeight - window.innerHeight - 1) ) {
const ms = new Date().getTime();
return {
url: `data/${sid}?_dc=${ms}`,
category: 'items',
method: 'GET'
};
}
return {};
});
const items$ = itemsXHR$
.fold( (acc=[],t) => [...acc, ...t.body.data.content.items] )
.startWith(null);
const vdom$ = items$.map(items =>
div('#items', [
ul('.search-results', items==null ? [] : items.map(data =>
li('.search-result', [
a({attrs: {href: `/${data.id}`}}, [
img({attrs: {src: data.url}})
])
])
))
])
);
The main issue is that the ajax request is linked to scroll position, and its possible that to fire multiple requests during scroll.
As the moment I debounce the scroll, but that it not ideal here.
Any ideas on how to orchestrate the streams so that only 1 request is sent when needed (when the user is near the bottom of the page)?
I though maybe a unique id per page and use .dropRepeats on the getitem$? I don't have a unique page id in the result though.
You could filter the scroll$ to take only the bottom of the page.
const onScroll$ = sources.DOM.select('document').events('scroll')
.map((e) => {
return e.srcElement.scrollingElement.scrollTop;
})
.filter( /* filter by checking scrollTop is near bottom */ )
.compose(debounce(1000));