Infinite scrolling and updating it with newly fetched data in React - reactjs

I'm trying to render an infinite scroll component and update its data with newly fetched news upon scrolling to the end of it. It works on the first scroll, but gets stuck at Loading... after that. I don't understand what is going on that makes it stop fetching after first scroll.
Code that's supposed to get new data looks like this:
const [latestNews, setLatestNews] = useState<any[]>([]);
const [page, setPage] = useState<number>(1);
const getLatestNews = async (page: number) => {
let url = `https://api.nytimes.com/svc/search/v2/articlesearch.json?api-key=${API_KEY}&page=${page}`;
const response = await fetch(url);
const data = await response.json();
setLatestNews(data.response.docs);
setPage(page + 1);
}
And my infinite scroll component looks like this
<InfiniteScroll dataLength={latestNews.length} next={() => getLatestNews(page)} hasMore={true} loader={<h4>Loading...</h4>} scrollableTarget="scrollableDiv">
{
latestNews.map((article, index) => (
<div className="latest-news-article flex flex-col gap-3 mb-4" key={index}>
<p className="latest-news-article-date">
{article.pub_date.slice(11, 16)}
</p>
<h1>
{article.headline.main}
</h1>
</div>
))
}
</InfiniteScroll>

I think I found the solution for now.
In my words, that scrolling was "too fast" and it updated the state quickly, much faster than API can take request and I was getting 429 Too Many Requests.
Solution:
const getLatestNews = async () => {
setTimeout(async () => {
const response = await fetch(`https://api.nytimes.com/svc/search/v2/articlesearch.json?api-key=${API_KEY}&page=${page}`);
const data = await response.json();
setLatestNews((prev) => [...prev, ...data.response.docs]);
setPage(page + 1);
}, 5000);
}
I added setTimeout to 5000ms, that way API requests are not sent instantly, and state changes just fine. It also renders new news accordingly.

Your solution works but there is another solution that is more logical.
You can add a ref using useRef and change its value when you're sending your request in order to prevent from sending another request at the same time:
const sendRequestRef = useRef(true);
const getLatestNews = async () => {
if(sendRequestRef.current === true) {
sendRequestRef.current = false;
const response = await fetch(`https://api.nytimes.com/svc/search/v2/articlesearch.json?api-key=${API_KEY}&page=${page}`);
const data = await response.json();
setLatestNews((prev) => [...prev, ...data.response.docs]);
setPage(page + 1);
sendRequestRef.current = true;
}
}

Related

React component doesn't render unless I refresh & duplicate render

I've been working on React/Redux app with firestore database
In my app I have simple POST request sent when the user send a message in the input field
and the data the user enters supposed to render in the same page without the need to refresh but I do still need to refresh even without deps in my useEffect!
Here's my code :
Post component
{posts.length > 0 &&
[...posts].map(({ id, data: { message, name, job, avatarUrl } }) => (
<Post
key={id}
name={name}
job={job}
message={message}
avatarUrl={avatarUrl}
/>
))}
However I also encounter a weird behavior after I refresh which is the components are rendered twice!Although my database be containing only one unique data for each messageThe react app renders it twice ( The querySnapshot from the database being added to the state arrayposts twice
useEffect
useEffect(() => {
querySnapshot();
});
}, []);
Database query:
const q = query(
collection(db, "posts"),
where("type", "==", "post"),
orderBy("postDate", "desc")
);
Retrieving the data :
const [input, setInput] = useState("");
const [posts, setPosts] = useState([]);
const [nextId, setNextId] = useState("0");
const addPost = (e) => {
e.preventDefault();
const docData = {
name: "mo",
job: "zoo",
message: input,
avatarUrl: "https://",
postDate: firebase.firestore.FieldValue.serverTimestamp(),
type: "post",
};
setDoc(doc(db, "posts", nextId.toString()), docData);
setNextId(parseInt(nextId) + 1);
setInput("");
};
async function querySnapshot() {
const querySnapshot = await getDocs(q);
console.log(querySnapshot.docs[0].data().message);
setNextId(querySnapshot.docs.length)
querySnapshot.docs.forEach((doc) => {
let data = {
id: doc.id,
data: doc.data(),
};
if (data && !posts.includes(data.id)) {
setPosts((current) => [...current, data]);
console.log("psts now", posts);
}
});
}
I tried to use the JavaScript Set by creating
useState(new Set()) but the same problem occurred of duplicate elements
I also tried to change deps of useEffect to render when the posts state array changes still not rendering untill I refresh
The duplication reason would be caused by setPosts(), I have updated the code as below, try to avoid setting the value inside the loop.
async function querySnapshot() {
const querySnapshot = await getDocs(q);
setNextId(querySnapshot.docs.length)
const data = querySnapshot.docs.map((doc)=>{
return {id:doc.id, data: doc.data()}
})
setPosts((current) => [...current, data])
}

Infinite scroll in React

I'm trying to add infinite scroll in my component. I somehow did it but it is not working as expected. Whenever, I scroll to bottom it adds more data to APIData but it just duplicates the data which has been already fetched instead of adding new data. And, even when I'm at bottom, the scroll function still runs and fetching the data again and again which has already been fetched. And I'm also getting a warning that I didn't include getData in the dependency array. What's happening and why?
export default function News({ category }) {
const [APIData, setAPIData] = useState([])
const [page, setPage] = useState(1)
const pageSize = 20
async function getData() {
let response = await fetch(`https://newsapi.org/v2/top-headlines?country=in&category=${category}&pageSize=${pageSize}&page=${page}&apiKey={APIKey}`)
let parsedResponse = await response.json()
setAPIData(APIData.concat(parsedResponse.articles))
}
useEffect(() => {
getData()
}, [category])
window.addEventListener('scroll', function () {
if (window.innerHeight + document.documentElement.scrollTop === document.scrollingElement.scrollHeight && page === 1) {
setPage(2)
getData()
console.log('working')
}
}
)
It can be most likely due to this https://reactjs.org/docs/state-and-lifecycle.html#state-updates-may-be-asynchronous
You can verify that what the page no while in the getData function by console
async function getData(category,page) {
let response = await fetch(`https://newsapi.org/v2/top-headlines?country=in&category=${category}&pageSize=${pageSize}&page=${page}&apiKey={APIKey}`)
let parsedResponse = await response.json()
setAPIData(APIData.concat(parsedResponse.articles))
}
useEffect(() => {
getData(category,page)
}, [category,page])
window.addEventListener('scroll', function () {
if (window.innerHeight + document.documentElement.scrollTop === document.scrollingElement.scrollHeight && page === 1) {
setPage(2)
console.log('working')
}
}
Add new items to state as follow:
setAPIData(previousState => [...previousState, ...parsedResponse.articles])

Why does my data not update when calling one api and updates well when I call another?

I am trying to create a real time covid-19 data dashboard using React. The data is coming from the desease.sh api and the global figures change regularly so I am sending a new request to the api end point to get new data using setInterval after a minute. But when that new request happens, the data does not change on my app.
Below is my code to the disease api. Which is not working as expected
function CoolTest() {
const [data, setData] = useState([]);
const getData = async () => {
try {
const data = await axios.get("https://disease.sh/v3/covid-19/all");
setData(data.data);
} catch (e) {
console.log(e)
}
};
useEffect(() => {
getData();
setInterval(()=>{
getData();
setData(data => data)
}, 120000)
}, []);
console.log(data.todayCases)
return (
<Container style={{marginTop:'15px'}}>
<h3>New Cases</h3>
<p className="text-muted"> {data.todayCases}</p>
<hr />
</Container>
)
}
Then using the same code, I tried making a request to the Chuck Norris jokes api, which is working. The Joke changes after every new request.
function CoolTest() {
const [jokes, setJokes] = useState([]);
const getJokesData = async () => {
try {
const data = await axios.get("https://api.chucknorris.io/jokes/random");
setJokes(data.data);
} catch (e) {
console.log(e)
}
};
useEffect(() => {
getJokesData();
setInterval(()=>{
getJokesData();
setJokes(jokes => jokes)
}, 120000)
}, []);
console.log(jokes.value)
return (
<Container style={{marginTop:'15px'}}>
<h3>Chuck Norris Jokes</h3>
<p className="text-muted"> {jokes.value}</p>
<hr />
</Container>
)
}
What am I missing?
Concole screenshot.
enter image description here
Data is not changing because the new data is the same as the previous.
you can change:
const data = await axios.get("https://disease.sh/v3/covid-19/all");
setData(data.data);
to
const data = await axios.get("https://disease.sh/v3/covid-19/all");
setData({ todayCases: Math.random() });
to see the view is changing.
in your second code api name is api.chucknorris.io/jokes/random
and gives you each time a random number that is not equal to the previous.
you can see results is this sandbox
and also change:
setInterval(()=>{
getJokesData();
}, 120000)
to:
setInterval(()=>{
getJokesData();
setJokes(jokes => jokes)
}, 120000)```

React re-renders list items in same card instead of a list of cards

I am doing a simple pokedex that should list all 1st gen pokemon, with the data being retrieved from https://pokeapi.co/
When I get to the screen, although I can see I have all the pokemon when I print, The list starts cycling and rerendering the same single card, instead of updating the list of cards and adding cards, instead of replacing the already existent one.
I think my issue is with understanding the React render cycle. Can anybody please point me in the right direction?
Below is the page where I have a method that gets a single pokemon from the api, and this is the link to my github repo https://github.com/vradu007/poke-frontend.
Thank you!
const MainPage = (props) => {
const [pokemons, setPokemons] = useState([]);
const getPokemon = async (pokemonId) => {
let pokemon = {};
try {
// const apiUrl = `https://pokeapi.co/api/v2/pokemon/${pokemon}`;
// const res = await axios.get(apiUrl);
// pokemons.push(res.data);
pokemon = fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonId}`)
.then((response) => response.json())
.then((response) => setPokemons([...pokemons, response]))
.then(() => {console.log(pokemons)});
} catch (error) {
console.log(error);
}
};
useEffect(() => {
for( let i = 0; i<151; i++){
getPokemon(i);
}
},[]);
return (
<React.Fragment>
<Header title="Pokemon Page" />
<PokemonList pokemons={pokemons} />
</React.Fragment>
);
};
This is because your setState is async, your state update is dependent on your previous state so you need to give a callback to your setPokemons function.
This solution isn't good as you are updating your state way too many times than needed. The better way is already posted by another user, that is to only setState when all of your promises are resolved.
pokemon = fetch(`https://pokeapi.co/api/v2/pokemon/${pokemonId}`)
.then((response) => response.json())
.then((response) => setPokemons(pokemons => [...pokemons, response]))
.then(() => {console.log(pokemons)});
Edit - It can be done with React (the way as posted by vanshaj) but here is another performant way to do this :-
const MainPage = (props) => {
const [pokemons, setPokemons] = useState([]);
const getPokemon = async (pokemonId) => {
try {
const apiUrl = `https://pokeapi.co/api/v2/pokemon/${pokemonId}`;
const res = await axios.get(apiUrl).then(res=>res.json());
return res;
} catch (error) {
console.log(error);
}
};
useEffect(() => {
async function gottaCatchEmAll(){
const promises = [];
for( let i = 0; i<151; i++){
promises.push(getPokemon(i));
}
const pokemons = await Promise.all(promises);
setPokemons(pokemons);
}
gottaCatchEmAll();
},[]);
return (
<React.Fragment>
<Header title="Pokemon Page" />
<PokemonList pokemons={pokemons} />
</React.Fragment>
);
};
In your implementation, you are trying to set the state in all the promises independently. What you need to ensure is that all these promises get resolved in an async manner as earlier but they are grouped together in a place such that when all of them get resolved ,a collective output can be returned which can be set as state in one pass. That grouping mechanism is Promise.all.

Conditionally calling an API using React-Query hook

I am using react-query to make API calls, and in this problem case I want to only call the API if certain conditions are met.
I have an input box where users enter a search query. When the input value is changed, a search server is called with the contents of the input as the search query ... but only if the input value is more than 3 chars long.
In my react component I'm calling:
const {data, isLoading} = useQuery(['search', searchString], getSearchResults);
And my getSearchResults function will, conditionally, make an API call.
const getSearchResults = async (_, searchString) => {
if (searchString.length < 3)
return {data: []}
const {data} = await axios.get(`/search?q=${searchString}`)
return data;
}
We can't use a hook inside a conditional - so I put the condition into my API calling function.
This almost works. If I enter a short query string, there is no API request made and I get an empty array back for the data. Yay!
But - isLoading will flip to true briefly - even though there is no HTTP request being made. So my loading indicator shows when there is no actual network activity.
Am I misunderstanding how to best solve my use case, is there a way to enure that isLoading will return false if there is no HTTP activity?
The key was to use Dependent Queries
So, in my main component, I create a boolean and pass that to the enabled option of the useQuery hook:
const isLongEnough = searchString.length > 3;
const {data, isLoading} = useQuery(['search', searchString], getSearchResults, {enabled: isLongEnough});
and the API calling method is simply the API call - not any conditional:
const getSearchResults = async (_, searchString) => {
const {data} = await axios.get(`/search?q=${searchString}`);
return data;
}
The docs describe dependent queries as a solution for loading data from subsequent API endpoints, but the enable option can accept any boolean. In this case - if the search query string is long enough.
There's another option which is to use queryClient.fetchQuery API, which gives you the ability to conditionally call the query to fetch the data.
function Example2() {
const [data, setData] = useState(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
if (isLoading) return "Loading...";
if (error) return "An error has occurred: " + error;
return (
<div>
<button
onClick={async () => {
try {
setIsLoading(true);
const posts = await queryClient.fetchQuery(
["postsUsingFetchQuery"],
{
queryFn: () =>
axios
.get("https://jsonplaceholder.typicode.com/posts")
.then((res) => res.data)
}
);
setData(posts);
} catch (e) {
setError(e);
}
setIsLoading(false);
}}
>
Fetch posts using fetchQuery{" "}
</button>
<h1>Posts</h1>
{data?.map((post) => {
return (
<div style={{ display: "flex" }}>
<span>{post.id}- </span>
<div>{post.title}</div>
</div>
);
})}
</div>
);
}
On the button click handler, we’ve added the implementation to fetch the posts using queryClient.fetchQuery.
You can read more from this link.

Resources