How to apply Infinite Scroll with react-intersection-observer - javascript

I'm using react-intersection-observer npm package for my CRA project.
I want to achieve this Infinite Scroll effect whenever users reach the bottom of the page or click on the Load More button -> It will fetch 8 more items.
Currently, the getMoreData() will only execute when I click on the Load More button.
Also, I put posts state in the dependency array so it will display the first 8 items for activePosts on start. But this leads to status code 429: over rate limit in the Network tab.
My Questions:
How to also apply the getMoreData() function to fetch out more items when we scroll to the end of the page? (Infinity scroll)
There might be a bug if I remove the posts from the dependency array, it won't display the first 8 items on initial page load. How to fix this the right way?
Screenshots:
My Code:
Posts.js
function Posts() {
const url = "https://6264f60294374a2c506b97c9.mockapi.io/posts";
const [posts, setPosts] = useState([]);
const [activePosts, setActivePosts] = useState([]);
const [isFetching, setIsFetching] = useState(false);
const getData = async () => {
try {
let response = await axios(url);
let result = response.data;
setPosts(result);
setActivePosts(posts.slice(0, 8));
} catch (err) {
console.log(err);
}
};
useEffect(() => {
getData();
}, [posts]);
const getMoreData = () => {
setIsFetching(true);
setTimeout(() => {
setActivePosts((prev) => {
return [...prev, ...posts.slice(prev.length + 1, prev.length + 9)];
});
setIsFetching(false);
}, 2000);
};
useEffect(() => {
if (!isFetching) return;
getMoreData();
}, [isFetching]);
return (
<>
<div className="posts">
{activePosts.map((post, index) => (
<Post post={post} key={post.id} index={index} />
))}
</div>
<button onClick={getMoreData}>
{isFetching ? "Loading..." : "Load more"}
</button>
</>
);
}
export default Posts;
Post.js
import { useInView } from "react-intersection-observer";
function Post({ post }) {
const { ref, inView } = useInView({
initialInView: true,
triggerOnce: true,
threshold: 1,
});
return (
<div className="post" ref={ref}>
{inView ?
<img src={post.imgUrl} alt={post.title} className="post__img" loading="lazy" />
: <div className="post__img" />
}
<h1 className="post__title">{post.title}</h1>
</div>
)
}
export default Post

Related

How to make a loading screen on react router?

I start learning react about 2 month ago. Right now I am trying to build my portfolio with some interactive design using spline 3d. The problem is the loading time is too long and I want to make a loading screen that stop loading exact time when my 3d start element render
There are multiple ways to create it by your self.
you can you use the library react-loader-spinner
on the console type npm install react-loader-spinner --save
import React from 'react';
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
import Loader from "react-loader-spinner";
import '../style.css';
const LoaderComponent = () => {
return (
<div className="loader">
<Loader
type="Circles"
color="#dc1c2c"
height={50}
width={100}
//timeout={1000} //3 secs
/>
</div>
);
};
export default LoaderComponent;
To display the component there are multiple ways, here is a way for GraphQL fetching data from the DB
const [results] = useQuery({ query: PRODUCT_QUERY });
const { data, fetching, error } = results;
//Check or the data coming in
if (fetching) return <p>Loading...</p>;
if (error) return <p>Oh no... {error.message}</p>;
Here is a way from fetching data with HTTP Request:
const UserList = () => {
const auth = useContext(AuthContext);
const { isLoading, error, sendRequest, clearError } = useHttpClient();
const [loadedUsers, setLoadedUsers] = useState();
useEffect(() => {
const fetchUsers = async () => {
try {
//with fetch, the default request type is GET request
const responseData = await sendRequest(
process.env.REACT_APP_BACKEND_URL + "/users"
);
setLoadedUsers(responseData.users); //users propeties is the given value from the backend (user-controllers.js on getUsers())
} catch (err) {}
};
fetchUsers();
}, [sendRequest]);
return (
<React.Fragment>
<ErrorModal error={error} onClear={clearError} />
{isLoading && <LoadingSpinner asOverlay />}
{/* we need to render loadedUsers only if not empty*/}
{!isLoading && loadedUsers && (
<div className="userList">
<span className="Title">Display Here the data</span>
</div>
)}
</React.Fragment>
);
};
// this logic is simple
// first, you have created one boolean usestate(false) and then load your screen that time usestate are true and process is complete after usesate are false
// I will show you the following example. I hope that helps you.
export default function Gradients(props) {
const [isLoading, setIsLoading] = useState(false);
const getAllGradient = () => {
setIsLoading(true);
axios
.get("https://localhost:5000")
.then((res) => {
const gradientColors = res.data;
// process complete after isLoading are false
// your process (this only example)
setIsLoading(false);
})
}
return(
<div>
{
isLoading ? <Loader> : <YourComponent />
}
</div>
)
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Unable to set state in a function

So I was trying to build a server side pagination that works like a static one and I'm almost there, But I've encountered some issues which I cannot seem to solve.
This is what my code looks like
const LiveIndex = (props) => {
const [currentPage, setCurrentPage] = useState(0);
const [isLoading, setLoading] = useState(false);
const startLoading = () => setLoading(true);
const stopLoading = () => setLoading(false);
useEffect(() => {
//After the component is mounted set router event handlers
Router.events.on("routeChangeStart", startLoading);
Router.events.on("routeChangeComplete", stopLoading);
return () => {
Router.events.off("routeChangeStart", startLoading);
Router.events.off("routeChangeComplete", stopLoading);
};
}, []);
const paginationHandler = (page) => {
const currentPath = props.router.pathname;
const currentQuery = props.router.query;
currentQuery.page = currentPage + 1;
props.router.push({
pathname: currentPath,
query: currentQuery,
});
setCurrentPage(currentQuery.page);
};
const backToLastPage = (page) => {
const currentPath = props.router.pathname;
const currentQuery = props.router.query;
currentQuery.page = currentPage - 1;
setCurrentPage(currentQuery.page); // THE code that breaks my code.
props.router.push({
pathname: currentPathh,
query: currentQueryy,
});
};
let content;
if (isLoading) {
content = (
<div>
<h2 class="loading-text">loading.</h2>
</div>
);
} else {
//Generating posts list
content = (
<div className="container">
<h2> Live Games - </h2>
<div className="columns is-multiline">
<p>{props.games.name}</p>
</div>
</div>
);
}
return (
<>
<div className={"container-md"}>
<div>{content}</div>
{props.games.length ? (
<a onClick={() => paginationHandler(currentPage)}> moore </a>
) : (
backToLastPage(currentPage)
)}
</div>
</>
);
};
export async function getServerSideProps({ query }) {
const page = query.page || 1; //if page empty we request the first page
const response = await fetch(
`exampleapi.com?sort=&page=${page}&per_page=10&token`
);
const data = await response.json();
return {
props: {
games: data,
},
};
}
export default withRouter(LiveIndex);
The issue is my backToLastPage does the job well but I'm unable to use setCurrentPage() in that function, Every time I use that I get the following error
Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop
How can I possibly update the value of my currentPage state in the backToLast function
Thank you
You're calling backToLastPage directly in JSX which will be re-rendered/re-called every time. And setCurrentPage (with useState) triggers re-rendering for state changes in backToLastPage.
You can imagine that every time the state changes, your component gets rendered and it will set states again that make infinite renderings for the component.
You can use useEffect to handle props.games changes. That will help you to trigger backToLastPage only once whenever props.games get changed.
React.useEffect(() => {
if(!props.games || !props.games.length) {
backToLastPage(currentPage)
}
},[props.games])
Full modification can be
const LiveIndex = (props) => {
const [currentPage, setCurrentPage] = useState(0);
const [isLoading, setLoading] = useState(false);
const startLoading = () => setLoading(true);
const stopLoading = () => setLoading(false);
useEffect(() => {
//After the component is mounted set router event handlers
Router.events.on("routeChangeStart", startLoading);
Router.events.on("routeChangeComplete", stopLoading);
return () => {
Router.events.off("routeChangeStart", startLoading);
Router.events.off("routeChangeComplete", stopLoading);
};
}, []);
//The main change is here
//It will be triggered whenever `props.games` gets updated
React.useEffect(() => {
if(!props.games || !props.games.length) {
backToLastPage(currentPage)
}
},[props.games])
const paginationHandler = (page) => {
const currentPath = props.router.pathname;
const currentQuery = props.router.query;
currentQuery.page = currentPage + 1;
props.router.push({
pathname: currentPath,
query: currentQuery,
});
setCurrentPage(currentQuery.page);
};
const backToLastPage = (page) => {
const currentPath = props.router.pathname;
const currentQuery = props.router.query;
currentQuery.page = currentPage - 1;
setCurrentPage(currentQuery.page); // THE code that breaks my code.
props.router.push({
pathname: currentPathh,
query: currentQueryy,
});
};
let content;
if (isLoading) {
content = (
<div>
<h2 class="loading-text">loading.</h2>
</div>
);
} else {
//Generating posts list
content = (
<div className="container">
<h2> Live Games - </h2>
<div className="columns is-multiline">
<p>{props.games.name}</p>
</div>
</div>
);
}
return (
<>
<div className={"container-md"}>
<div>{content}</div>
{props.games.length && (
<a onClick={() => paginationHandler(currentPage)}> moore </a>
)}
</div>
</>
);
};
export async function getServerSideProps({ query }) {
const page = query.page || 1; //if page empty we request the first page
const response = await fetch(
`exampleapi.com?sort=&page=${page}&per_page=10&token`
);
const data = await response.json();
return {
props: {
games: data,
},
};
}
export default withRouter(LiveIndex);

useEffect with many dependencies

I am trying to fetch employees and here is what I am trying to do using useEffect
function AdminEmployees() {
const navigate = useNavigate();
const dispatch = useDispatch();
// fetching employees
const { adminEmployees, loading } = useSelector(
(state) => state.adminFetchEmployeeReducer
);
useEffect(() => {
dispatch(adminFetchEmployeeAction());
if (adminEmployees === "unAuthorized") {
navigate("/auth/true/false");
}
}, [adminEmployees, navigate,dispatch]);
console.log("Here i am running infinie loop");
console.log(adminEmployees);
return (
<>
{loading ? (
<Loader></Loader>
) : adminEmployees === "no employees" ? (
<h1>No Employees</h1>
) : (
<>
{adminEmployees &&
adminEmployees.map((employee) => {
return (
<div className="admin__employee__container" key={employee.id}>
<AdminSingleEmployee
employee={employee}
></AdminSingleEmployee>
</div>
);
})}
</>
)}
</>
);
}
Here I want to achieve 2 goals:
fetch adminEmployees
if (adminEmployees==='unAuthorized') then go to loginPage
but when doing this as in the code, it creates infinite loop.
How can I achieve the desired functionality?
Easy dirty path: split useEffect into 2
useEffect(() => {
if (adminEmployees === "unAuthorized") {
navigate("/auth/true/false");
}
}, [adminEmployees, navigate]);
useEffect(() => {
dispatch(adminFetchEmployeeAction());
}, [dispatch]);
Better way: handle that case in reducer or action creator to flip flag in the store and then consume it in component:
const { shouldNavigate } = useSelector(state => state.someSlice);
useEffect(() => {
if(shouldNavigate) {
// flipping flag back
dispatch(onAlreadyNavigated()));
navigate("/yourPath...");
},
[navigate, dispatch, shouldNavigate]
);

Search box with React-Query

I am trying to implement Product search by text. Fetching data with react-query. The following implementation is working but it does not feel right to me. Let me know if I am overdoing it and if there is a simpler solution with react-query.
import { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import { useQueryClient } from 'react-query';
import ProductCard from '#/components/cards/ProductCard';
import { useQueryProducts } from '#/hooks/query/product';
import { selectSearch } from '#/store/search';
// function fetchProductsByFilter(text){}
const Shop = ({ count }) => {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
const { text } = useSelector(selectSearch);
const productsQuery = useQueryProducts(count);
useEffect(() => {
setProducts(productsQuery.data);
setLoading(false);
}, []);
const queryClient = useQueryClient();
useEffect(() => {
const delayed = setTimeout(() => {
queryClient.prefetchQuery(['searchProductsByText'], async () => {
if (text) {
const data = await fetchProductsByFilter(text);
setProducts(data);
setLoading(false);
return data;
}
});
}, 300);
return () => clearTimeout(delayed);
}, [text]);
return (
<div className="container-fluid">
<div className="row">
<div className="col-md-3">search/filter menu</div>
<div className="col-md-9">
{loading ? (
<h4 className="text-danger">Loading...</h4>
) : (
<h4 className="text-danger">Products</h4>
)}
{products.length < 1 && <p>No products found</p>}
<div className="row pb-5">
{products.map((item) => (
<div key={item._id} className="col-md-4 mt-3">
<ProductCard product={item} />
</div>
))}
</div>
</div>
</div>
</div>
);
};
// async function getServerSideProps(context) {}
export default Shop;
It doesn't seem very idiomatic to me. With react-query, the key to using filters are to put them into the query key. Since react-query refetches every time the key changes, you'll get a refetch every time you change a filter, which is usually what you want. It's a very declarative way of doing things. No useEffect needed at all.
If this happens when choosing something from a select or clicking an apply button, that's really all you need:
const [filter, setFilter] = React.useState(undefined)
const { data, isLoading } = useQuery(
['products', filter],
() => fetchProducts(filter)
{ enabled: Boolean(filter) }
)
Here, I am additionally disabling the query as long as the filter is undefined - fetching will start as soon as we call setFilter.
if typing into a text field is involved, I'd recommend some debouncing to avoid firing off too many requests. The useDebounce hook is very good for that. You'd still have the useState, but you'd use the debounced value for the query:
const [filter, setFilter] = React.useState(undefined)
const debouncedFilter = useDebounce(filter, 500);
const { data, isLoading } = useQuery(
['products', debouncedFilter],
() => fetchProducts(debouncedFilter)
{ enabled: Boolean(debouncedFilter) }
)
If this happens when choosing something from a select or clicking an apply button, that's really all you need:
const [filter, setFilter] = useState<string>('')
const isEnabled = Boolean(filter)
const { data, isLoading } = useQuery(
['products', filter],
() => fetchProducts(filter)
{ enabled: enabled: filter ? isEnabled : !isEnabled, }
)
<input type="text" onChange={(e) => setFilter(e.target.value)}/>

How to use InfiniteScroll in React

My idea is : when I click into the page, the page will send a axios request for the top 30 data and show them in the InfoCard. After that, when I scroll to the end of the page it will send a new axios request for the next 30 data and show them in the InfoCard.
I watched this tutorial and tried it myself but I still not sure how it works.
https://www.youtube.com/watch?v=NZKUirTtxcg
//TestScreen3.js
function TestScreen3() {
const [topNdata, setTopNdata] = useState(30)
const [skipNdata, setSkipNdata] = useState(0)
const { loading, ScenicSpot, hasMore } = RequestTest(topNdata, skipNdata)
return (
<div className="App">
<header className="App-header">
<Navbar NavbarTitle="Scenic Spots" />
<CityList />
{ScenicSpot.map((infoCard) => (<InfoCard key={infoCard.ID} Name={infoCard.Name} Description={infoCard.Description} Picture={infoCard.Picture.PictureUrl1} />))}
</header>
</div>
);
}
export default TestScreen3;
//RequestTest.js
export default function RequestTest(topNdata, skipNdata) {
const [ScenicSpot, setScenicSpot] = useState([])
const [hasMore, setHasMore] = useState(false)
const [loading, setLoading] = useState(true)
useEffect(() => {
setScenicSpot([])
}, [topNdata])
useEffect(() => {
setLoading(true)
axios({
method: 'GET',
url: 'https://ptx.transportdata.tw/MOTC/v2/Tourism/ScenicSpot',
params: { $top: topNdata, $skip: skipNdata },
}).then(res => {
setScenicSpot(res.data)
setHasMore(res.data.length > 0)
setLoading(false)
}).catch(err => { console.log(err) })
}, [topNdata, skipNdata])
return { loading, ScenicSpot, hasMore }
}
Though it will seem like a different thing.
But you will found a way for infinite scroll here at StackOverflow.
Read the question and my answer to this question.
Though I was using axios from my API, you will get the idea from there.

Categories

Resources