Infinite Scroll with react and api calls updating state - javascript

I am trying to take a tutorial for an infinite scroll using vanilla javascript and use react. To get a better understanding of how react works. I can fetch the data display the initial data. Scroll to the bottom fetch more data but the data just over riders the current data. Also I can only fetch up to page 2 I would love if someone could point me in the right direction.
import React, { useState, useEffect } from "react";
import "./App.css";
function App() {
const [posts, setPosts] = useState([{}]);
const [isFetching, setIsFetching] = useState(false);
let limit = 5;
let page = 1;
const getPosts = async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?_limit=${limit}&_page=${page}`
);
const data = await response.json();
setPosts(data);
console.log(data);
};
function handleScroll() {
if (
window.innerHeight + document.documentElement.scrollTop !==
document.documentElement.offsetHeight
)
return;
setIsFetching(true);
}
function getMorePosts() {
setTimeout(() => {
page++;
setPosts([{ ...posts }], posts);
setIsFetching(false);
}, 2000);
}
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, []);
useEffect(
() => {
getPosts();
}, //eslint-disable-next-line
[]
);
useEffect(() => {
if (!isFetching) return;
getMorePosts();
}, [isFetching]);
return (
<div className="App">
{posts.map((post, index) => (
<div key={index} className="post">
<div className="number">{post.id}</div>
<div className="post-info">
<h2 className="post-title">{post.title}</h2>
<p className="post-body">{post.body}</p>
</div>
</div>
))}
{isFetching && (
<div className="loader">
<div className="circle"></div>
<div className="circle"></div>
<div className="circle"></div>
</div>
)}
</div>
);
}
export default App;

One thing I noticed off the bat is that page is not in the state so it will be reset on every render. Also since limit is not changing you should use a constant.
Why are you initializing this to an array with an empty object in it? useState([{}]); just use an empty array
Also I'm not sure what you are intending to do here setPosts([{ ...posts }], posts); but if you want to append the new posts while copying the objects you should do this
const getPosts = async () => {
setIsFetching(true)
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?_limit=${limit}&_page=${page}`
);
const data = await response.json();
setPosts([...posts, ...data]);
setIsFetching(false)
};
function getMorePosts() {
setTimeout(() => {
setPage(page++)
getPosts();
}, 2000);
}

Related

I want to paginate data. It is working fine but when I search for specific data it always shows in the first page

What I want is to paginate my data but the problem is when I'm searching for specific data if I'm on page 3 the result shows on page 1 always and I can't see anything because I was on page no 3. I want to go to page 1 automatically when I'm searching for something. Also when I press the next button if there is no data at all it still increases the page number.
Here is my code:
import { React, useState, useEffect } from "react";
import UpdateDialogue from "./UpdateDialogue";
function List(props) {
const API_URL = "http://dummy.restapiexample.com/api/v1/employees";
const [EmployeeData, setEmployeeData] = useState([]);
const [pageNumber, setPageNumber] = useState(1);
const [postNumber] = useState(8);
const currentPageNumber = pageNumber * postNumber - postNumber;
const handlePrev = () => {
if (pageNumber === 1) return;
setPageNumber(pageNumber - 1);
};
const handleNext = () => {
setPageNumber(pageNumber + 1);
};
useEffect(() => {
fetch(API_URL)
.then((response) => response.json())
.then((response) => {
setEmployeeData(response.data);
})
.catch((err) => {
console.error(err);
});
}, []);
const filteredData = EmployeeData.filter((el) => {
if (props.input === "") {
return el;
} else {
return el.employee_name.toLowerCase().includes(props.input)
}
});
const paginatedData = filteredData.splice(currentPageNumber, postNumber);
return (
<>
<ul>
{paginatedData.map((user) => (
<UpdateDialogue user={user} key={user.id} />
))}
</ul>
<div>Page {pageNumber} </div>
<div>
<button style={{marginRight:10}} onClick={handlePrev}>prev</button>
<button onClick={handleNext}>next</button>
</div>
</>
);
}
export default List;
Maybe with a useEffect on your input:
useEffect(() => {
if (props.input) {
setPageNumber(1);
}
}, [props.input]);
That way, whenever your input changes, your page number is set to 1.

Proper way to fetch from another component - ReactJS

I have a component that fetches the data properly but I want to encapsulate it in a helper. I've tried many things but I'm stuck.
This is the component that works:
export const Carousel = () => {
const [ lotteries, setLotteries ] = useState({});
const [ isLoading, setisLoading ] = useState(true);
useEffect(() => {
async function fetchAPI() {
const url = 'https://protected-sea-30988.herokuapp.com/https://www.lottoland.com/api/drawings;'
let response = await fetch(url)
response = await response.json()
setLotteries(response)
setisLoading(false)
}
fetchAPI()
}, [])
return (
<>
{
isLoading ? (
<span>loading</span>
) : (
<Slider >
{
Object.keys(lotteries).map((lottery, idx) => {
return (
<Slide
key={ idx }
title={ lottery }
prize={ lotteries[lottery].next.jackpot }
day={ lotteries[lottery].next.date.day }
/>
)
})
}
</Slider>
)}
</>
);}
And this is the last thing I've tried so far. This is the component without the fetch
export const Carousel = () => {
const [ lotteries, setLotteries ] = useState({});
const [ isLoading, setIsLoading ] = useState(true);
useEffect(() => {
getLotteries()
setLotteries(response)
setIsLoading(false)
}, [])
And this is where I tried to encapsulate the fetching.
export const getLotteries = async() => {
const url = 'https://protected-sea-30988.herokuapp.com/https://www.lottoland.com/api/drawings;'
let response = await fetch(url)
response = await response.json()
return response;
}
I'm a bit new to React, so any help would be much appreciated. Many thanks.
To get the fetched data from getLotteries helper you have to return a promise
export const getLotteries = async() => {
const url = 'https://protected-sea-
30988.herokuapp.com/https://www.lottoland.com/api/drawings;'
let response = await fetch(url)
return response.json()
}
and call it as async/await
useEffect(async() => {
let response= await getLotteries()
setLotteries(response)
setIsLoading(false)
}, [])
If you want to separate the logic for requesting a URL into another helper function, you can create a custom hook.
// customHook.js
import { useEffect, useState } from 'react';
export function useLotteries() {
const [lotteries, setLotteries] = useState(null);
useEffect(() => {
fetch('https://protected-sea-30988.herokuapp.com/https://www.lottoland.com/api/drawings;')
.then(response => response.json())
.then(json => setLotteries(json));
}, []);
return lotteries;
}
// Carousel.js
import { useLotteries } from "./customHook.js";
export const Carousel = () => {
const lotteries = useLotteries();
if (lotteries) {
return; /* Your JSX here! (`lotteries` is now contains all the request responses) */
} else {
return <Loader />; // Or just null if you don't want to show a loading indicator when your data hasn't been received yet.
}
};

Make React JS await for a async func to complete before running

I'm trying to make react not load until after an axios get requests finishes. I'm pretty rough on react all around, so sorry in advance.
I'm getting an array of objects
const { dogBreedsTest } = useApplicationData()
And I need it to be the default value of one of my states
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest);
However, I'm getting an error that my value is coming up as null on the first iteration of my app starting. How can I ensure that my value has completed my request before my app tries to use it?
Here is how I am getting the data for useApplicationData()
const [dogBreedsTest, setDogBreeds] = useState(null);
const getDogBreeds = async () => {
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
setDogBreeds
}
And I am importing into my app and using:
import useApplicationData from "./hooks/useApplicationData";
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest[0]);
const [breedList1, updateBreedList1] = useState(dogBreedsTest[0])
function handleOnDragEnd(result) {
if (!result.destination) return;
const items = Array.from(dogBreeds);
const [reorderedItem] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, reorderedItem);
for (const [index, item] of items.entries()) {
item['rank'] = index + 1
}
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0])
}
return (
<div className="flex-container">
<div className="App-header">
<h1>Dog Breeds 1</h1>
<DragDropContext onDragEnd={handleOnDragEnd}>
<Droppable droppableId="characters">
{(provided) => (
<ul className="dogBreeds" {...provided.droppableProps} ref={provided.innerRef}>
{breedList1?.map(({id, name, rank}, index) => {
return (
<Draggable key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable>
);
})}
{provided.placeholder}
</ul>
)}
</Droppable>
</DragDropContext>
</div>
)
error: TypeError: Cannot read property 'map' of null
(I am mapping the data later in the program)
const getDogBreeds = async () => {
try {
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
}
useEffect(() => {
getDogBreeds() // -> you are not awaiting this
}, []);
Do this instead
useEffect(() => {
axios.get('https://dog.ceo/api/breeds/list/all')
.then(res => {
const newDogList = generateDogsArray(res.data['message']);
const generatedDogs = selectedDogs(newDogList);
setDogBreeds(generatedDogs);
})
.catch(err => console.log(err));
}, []);
I know this looks awful, but I don't think you should use async/await inside useEffect
Use this in your application
useEffect will update whenever dogBreedsTest is changed. In order to make it work, start with null values and update them to the correct initial values once your async operation is finished.
const { dogBreedsTest } = useApplicationData();
const [dogBreeds, updateDogBreeds] = useState(null);
const [breedList1, updateBreedList1] = useState(null);
useEffect(() => {
updateDogBreeds(dogBreedsTest[0]);
updateBreedList1(dogBreedsTest[0]);
}, [dogBreedsTest]);
The problem is, that react first render and then run useEffect(), so if you don't want to render nothing before the axios, you need to tell to react, that the first render is null.
Where is your map function, to see the code? to show you it?.
I suppose that your data first is null. So you can use something like.
if(!data) return null
2nd Option:
In your map try this:
{breedList1 === null
? null
: breedList1.map(({id, name, rank}, index) => (
<Draggable
key={id} draggableId={id} index={index}>
{(provided) => (
<li ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps}>
<p>
#{rank}: { name }
</p>
</li>
)}
</Draggable> ))}
You have null, because your axios is async and react try to render before any effect. So if you say to react that the list is null, react will render and load the data from the api in the second time.
Option 1 use the optional chaining operator
dogBreedsTest?.map()
Option 2 check in the return if dogBreedsTest is an array
retrun (<>
{Array.isArray(dogBreedsTest) && dogBreedsTest.map()}
</>)
Option 3 return early
if (!Array.isArray(dogBreedsTest)) return null
retrun (<>
{dogBreedsTest.map()}
</>)
Option 4 set initial state
const [dogBreedsTest, setDogBreeds] = useState([]);
You could also add a loading state and add a loading spinner or something like that:
const [dogBreedsTest, setDogBreeds] = useState(null);
const [loading, setLoading] = useState(true)
const getDogBreeds = async () => {
setLoading(true)
try{
const { data } = await axios.get('https://dog.ceo/api/breeds/list/all')
if(data) {
const newDogList = generateDogsArray(data['message'])
const generatedDogs = selectedDogs(newDogList)
setDogBreeds(generatedDogs)
}
} catch(err) {
console.log(err);
}
setLoading(false)
}
useEffect(() => {
getDogBreeds()
}, []);
return {
dogBreedsTest,
loading,
setDogBreeds
}
Edit
Try to use a useEffect hook to update the states when dogBreedsTest got set.
const { dogBreedsTest } = useApplicationData()
const [dogBreeds, updateDogBreeds] = useState(dogBreedsTest?.[0] ?? []);
const [breedList1, updateBreedList1] = useState(dogBreedsTest?.[0] ?? [])
useEffect(() => {
updateDogBreeds(dogBreedsTest?.[0] ?? [])
updateBreedList1(dogBreedsTest?.[0] ?? [])
}, [dogBreedsTest])

window.addEventListener doesn't update state as expected

I want to build infinity scroll from scratch.Here is code:
import React,{useEffect, useRef, useState} from 'react';
const Container:React.FC<{}> = () => {
const containerRef = useRef<HTMLDivElement | null>(null)
const [ scrollNumber, setScrollNumber ] = useState<number>(1);
const [posts, setPosts] = useState<any[]>([]);
useEffect(() => {
async function main(){
const req = await fetch('https://jsonplaceholder.typicode.com/posts');
const result = await req.json();
const pst = result.splice(0,10*scrollNumber);
setPosts(pst)
function scrollHandler(){
if(containerRef.current?.getBoundingClientRect().bottom === window.innerHeight){
console.log("Salam", scrollNumber);
setScrollNumber(scrollNumber + 1);
}
}
window.addEventListener("scroll", scrollHandler);
return () => window.removeEventListener("scroll", scrollHandler);
}
main()
},[])
useEffect(() => {
const loadData = async () => {
const req = await fetch('https://jsonplaceholder.typicode.com/posts');
const result = await req.json();
const pst = result.splice(0,10*scrollNumber);
setPosts(pst)
}
loadData()
},[scrollNumber])
return (
<div ref={containerRef}>
{posts.map(post => (
<div key={post.id}>
<br/>
<h1>{post.title}</h1>
(<p>{post.body}</p>)
<code>{post.userId}</code>
<br/><br/>
<hr/>
</div>
))}
</div>
)
}
export default Container
But there is a problem. When I test it. it always updates to 2 nothing else. I searched for it. I found that window.addEventListener remembers only initialState that's why state always updates to 2. But I couldn't find any code solution.
inside the first useEffect the dependency array is empty, which mean if you use any state variable inside it, the value of it not gonna change inside the effect
As I see you are using scrollNumber to set the next state of setScrollNumber(scrollNumber + 1); and value of scrollNumber is not gonna change inside effect because of the dependency array is empty
So you have to tell the react to reinitialize the effect if you have any changes to the scrollNumber
so put scrollNumber as an dependency will work for you
useEffect(() => {
...
function scrollHandler(){
if(containerRef.current?.getBoundingClientRect().bottom === window.innerHeight) {
console.log("Salam", scrollNumber);
setScrollNumber(scrollNumber + 1);
}
}
window.addEventListener("scroll", scrollHandler);
return () => window.removeEventListener("scroll", scrollHandler);
.....
}, [scrollNumber]) //add a dependency

React programmatically recall a hook

https://codesandbox.io/s/react-hooks-usefetch-cniul
Please see above url for a very simplified version of my code.
I want to be able to refetch data from an API with my hook, within an interval (basically poll an endpoint for data).
What I want is to be able to just call something like refetch (as I've shown in the code as a comment), which would essentially just call fetchData again and update state with the response accordingly.
What's the best way to go about this? The only way I can think of is to add a checker variable in the hook which would be some sort of uuid (Math.random() maybe), return setChecker as what is refetch and just add checker to the array as 2nd useEffect argument to control rerendering. So whenever you call refetch it calls setChecker which updates the random number (checker) and then the function runs again.
Obviously this sounds "hacky", there must be a nicer way of doing it - any ideas?
If you want to have a constant poll going, I think you can move the setInterval() into the hook like so:
function useFetch() {
const [data, setDataState] = useState(null);
const [loading, setLoadingState] = useState(true);
useEffect(() => {
function fetchData() {
setLoadingState(true);
fetch(url)
.then(j => j.json())
.then(data => {
setDataState(data);
setLoadingState(false);
});
}
const interval = setInterval(() => {
fetchData();
}, 5000);
fetchData();
return () => clearInterval(interval);
}, []);
return [
{
data,
loading
}
];
}
Remember to include the return () => clearInterval(interval); so the hook is cleaned up correctly.
import React, { useEffect, useState, useCallback } from "react";
import ReactDOM from "react-dom";
const url = "https://api.etilbudsavis.dk/v2/dealerfront?country_id=DK";
function useFetch() {
const [data, setDataState] = useState(null);
const [loading, setLoadingState] = useState(true);
const refetch = useCallback(() => {
function fetchData() {
console.log("fetch");
setLoadingState(true);
fetch(url)
.then(j => j.json())
.then(data => {
setDataState(data);
setLoadingState(false);
});
}
fetchData();
}, []);
return [
{
data,
loading
},
refetch
// fetchData <- somehow return ability to call fetchData function...
];
}
function App() {
const [
{ data, loading },
refetch
// refetch
] = useFetch();
useEffect(() => {
const id = setInterval(() => {
// Use the refetch here...
refetch();
}, 5000);
return () => {
clearInterval(id);
};
}, [refetch]);
if (loading) return <h1>Loading</h1>;
return (
<>
<button onClick={refetch}>Refetch</button>
<code style={{ display: "block" }}>
<pre>{JSON.stringify(data[0], null, 2)}</pre>
</code>
</>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Maybe the following will work, it needs some adjustments to useFetch but you can still call it normally in other places.
//maybe you can pass url as well so you can use
// it with other components and urls
function useFetch(refresh) {
//code removed
useEffect(() => {
//code removed
}, [refresh]);
//code removed
}
const [refresh, setRefresh] = useState({});
const [{ data, loading }] = useFetch(refresh);
useEffect(() => {
const interval = setInterval(
() => setRefresh({}), //forces re render
5000
);
return () => clearInterval(interval); //clean up
});
Simple answer to question:
export default function App() {
const [entities, setEntities] = useState();
const [loading, setLoadingState] = useState(true);
const getEntities = () => {
setLoadingState(true);
//Changet the URL with your own
fetch("http://google.com", {
method: "GET",
})
.then((data) => data.json())
.then((resp) => {
setEntities(resp);
setLoadingState(false);
});
};
useEffect(() => {
const interval = setInterval(() => {
getEntities();
}, 5000);
return () => clearInterval(interval);
}, []);
}

Categories