I am trying to implement "infinite scroll" in my react app in which I fetch all data at once and as user scrolls down the page it displays more and more data. For that I use Intersection Observer in my custom hook with threshold of 1 to detect when user scrolls to end of "section" element so that I then can display more data. The problem is that after initial data is rendered my Intersection observer doesn't fire anymore as if it's disconnected but it's not.
Here is my custom hook:
import {useCallback, useEffect, useState} from "react";
export function useInfinityScrollObserver(ref) {
let [isVisible, setIsVisible] = useState(false);
const handleIntersection = useCallback(([entry]) => {
if (entry.isIntersecting){
setIsVisible(true)
}else if (!entry.isIntersecting){
setIsVisible(false)
}
}, [])
useEffect(() => {
const options = {
threshold: 1
}
// Create the observer, passing in the callback
const observer = new IntersectionObserver(handleIntersection, options);
// If we have a ref value, start observing it
if (ref.current) {
observer.observe(ref.current);
}
// If unmounting, disconnect the observer
return () => {
observer.unobserve(ref.current)
observer.disconnect();
}
}, [handleIntersection]);
return isVisible;
}
And here is component where I fetched data and I wanna display more data when user scrolls to the end of the "section" element:
const CountriesSection = () => {
let [data ,setData] = useState(null)
let [loadedCountries, setLoadedCountries] = useState([])
let [loadedCountriesNum, setLoadedCountriesNum] = useState(0)
const ref = useRef(null);
const isVisible = useInfinityScrollObserver(ref) // set hook to watch "section" ref
// Fetch all data at once
useEffect(() => {
async function fetchData() {
const response = await fetch(`https://disease.sh/v3/covid-19/countries?sort=cases`)
const countries = await response.json()
setData(countries)
}
fetchData()
}, [])
// When initial data is fetched or when number of Countries we wanna display changes fire this useEffect
useEffect(() => {
if (data){
const nextCountriesToShow = data.slice(loadedCountriesNum, loadedCountriesNum + 20)
.map(country => <CountryCard key={country.countryInfo._id}
country={country.country}
flag={country.countryInfo.flag}
continent={country.continent}
infected={country.cases}
recovered={country.recovered}
deaths={country.deaths} />)
setLoadedCountries(previousCountries => {
return [...previousCountries, ...nextCountriesToShow]
})
}
}, [data, loadedCountriesNum])
// When Intersection Observer in custom hook changes its state fire this effect. Which means when user scrolls down to the end of "section" element
useEffect(() => {
if (isVisible){
setLoadedCountriesNum(previousNum => {
return previousNum + 20
})
}
}, [isVisible])
return (
<section ref={ref} className={styles['section-countries']}>
{loadedCountries}
</section>
)
}
export default CountriesSection;
One more thing I noticed is when I change threshold inside of my custom hook from 1 to 0, then additional data is rendered each time when section enters my viewport.
Related
I am using ResizeObserver to call a function when the screen is resized, but I need to get the updated value of a state within the observer in order to determine some conditions before the function gets invoked.
It's something like this:
let [test, setTest] = React.useState(true)
const callFunction = () => {
console.log('function invoked')
setTest(false) // => set 'test' to 'false', so 'callFunction' can't be invoked again by the observer
}
const observer = React.useRef(
new ResizeObserver(entries => {
console.log(test) // => It always has the initial value (true), so the function is always invoked
if (test === true) {
callFunction()
}
})
)
React.useEffect(() => {
const body = document.getElementsByTagName('BODY')[0]
observer.current.observe(body)
return () => observer.unobserve(body)
}, [])
Don't worry about the details or why I'm doing this, since my application is way more complex than this example.
I only need to know if is there a way to get the updated value within the observer. I've already spent a considerable time trying to figure this out, but I couldn't yet.
Any thoughts?
The problem is, you are defining new observer in each re render of the component, Move it inside useEffect will solve the problem. also you must change this observer.unobserve(body) to this observer..current.unobserve(body).
I have created this codesandbox to show you how to do it properly. this way you don't need external variable and you can use states safely.
import { useEffect, useState, useRef } from "react";
const MyComponent = () => {
const [state, setState] = useState(false);
const observer = useRef(null);
useEffect(() => {
observer.current = new ResizeObserver((entries) => {
console.log(state);
});
const body = document.getElementsByTagName("BODY")[0];
observer.current.observe(body);
return () => observer.current.unobserve(body);
}, []);
return (
<div>
<button onClick={() => setState(true)}>Click Me</button>
<div>{state.toString()}</div>
</div>
);
};
export default MyComponent;
I am trying to implement a simple search algorithm for my products CRUD.
The way I thought to do it was entering the input in a search bar, and the products that matched the search would appear instantly every time the user changes the input, without needing to hit a search button.
However, the way I tried to do it was like this:
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
const queryProducts = filterProducts(queryString, productList);
setProductList(queryProducts);
}, [queryString, productList]);
I know that productList changes every render, and that's probably why it isn't working. But I didn't figure out how can I solve the problem. I've seen other problems here and solutions with useReducer, but I none of them seemed to help me.
The error is this one below:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
what you are doing here is fetching a product list and filtering it based on the query string and using that filtered list to render the UI. So ideally your filteredList is just a derived state based on your queryString and productList. So you can remove the filterProducts from your useEffect and move it outside. So that it runs when ever there is a change in the state.
function filterProducts (productName = '', productList = []) {
return productName.trim().length > 0 ? productList.filter((prod)=> {
return prod.title === productName;
}); : productList
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [queryString, setQueryString] = useState('');
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
}
}, [queryString]);
// query products is the derived state
const queryProducts = filterProducts(queryString, productList);
// Now instead of using productList to render something use the queryProducts
return (
{queryProducts.map(() => {
.....
})}
)
If you want the filterProducts to run only on change in queryString or productList then you can wrap it in useMemo
const queryProducts = React.useMemo(() => filterProducts(queryString, productList), [queryString, productList]);
When you use a setState function in a useEffect hook while having the state for that setState function as one of the useEffect hook's dependencies, you'll get this recursive effect where you end up infinitely re-rendering your component.
So, first of all we have to remove productList from the useEffect. Then, we can use a function to update your state instead of a stale update (like what you're doing in your example).
function filterProducts (productName, productList) {
const queryProducts = productList.filter((prod)=> {
return prod.title === productName;
});
return queryProducts;
}
function HomePage () {
const [productList, setProductList] = useState([]);
const [popupTrigger, setPopupTrigger] = useState('');
const [productDeleteId, setProductDeleteId] = useState('');
const [queryString, setQueryString] = useState('');
let history = useHistory();
useEffect(() => {
if (queryString.trim() === "") {
Axios.get("http://localhost:3001/api/product/get-all").then((data) => {
setProductList(data.data);
});
return;
}
setProductList(prevProductList => {
return filterProducts(queryString, prevProductList)
});
}, [queryString]);
Now, you still get access to productList for your filter, but you won't have to include it in your dependencies, which should take care of the infinite re-rendering.
I recommend several code changes.
I would separate the state that immediately reflects the user input at all times from the state that represents the query that is send to the backend. And I would add a debounce between the two states. Something like this:
const [query, setQuery] = useState('');
const [userInput, setUserInput] = useState('');
useDebounce(userInput, setQuery, 750);
I would split up the raw data that was returned from the backend and the filtered data which is just derived from it
const [products, setProducts] = useState([]);
const [filteredProducts, setFilteredProducts] = useState([]);
I would split up the useEffect and not mix different concerns all into one (there is no rule that you cannot have multiple useEffect)
useEffect(() => {
if (query.trim() === '') {
Axios
.get("http://localhost:3001/api/product/get-all")
.then((data) => { setProducts(data.data) });
}
}, [query]);
useEffect(
() => setFilteredProducts(filterProducts(userInput, products)),
[userInput, products]
);
I have a component that displays list of recipes in my firestore database, I am using react-infinite-scroll component package.
Everything works, however once I click on one of the items it takes me to another component and when I go back with react router, useEffect starts calling for items on scroll again, even though all data is displayed already.
How Can I build a logic that once I return to the component it remembers where I left off?
const [mealSearchResults, setMealSearchResults] = useContext(
mealSearchResultsContext
);
const [latestMealDoc, setLatestMealDoc] = useContext(latestMealDocContext);
const getNextMeals = async () => {
const ref = db
.collection("meals")
.orderBy("timestamp")
.limit(6)
.startAfter(latestMealDoc || 0);
const data = await ref.get();
data.docs.forEach((doc) => {
const meal = doc.data();
setMealSearchResults((prev: any) => [...prev, meal]);
});
setLatestMealDoc((prev: any) => data.docs[data.docs.length - 1]);
};
useEffect(() => {
getNextMeals();
}, []);
I have a component that uses action and repository to request data from rails server and then render it in component, it returns promise, that I've handle and render:
const {loadNews} = props;
const [news, setNews] = useState([]);
const [newsState, setNewsState] = useState('active');
const [pageNum, setPageNum] = useState(1);
where {loadNews} - function, that imports from parent component; Others are state, where:
news - array with news, that map's in different component in render;
newsState- news field state (e.g. 'active', or 'past');
pageNum- pagination state for adding more news in component;
I'm use he next code for update state:
const locationHashChanged = () => {
if (window.location.hash === "#state-active") {
setNewsState('active');
setPageNum(pageNum);
}
else if (window.location.hash == "#state-played") {
setNewsState('played');
setPageNum(pageNum);
}
}
window.onhashchange = locationHashChanged;
const changeHash = () => {
setNews([]);
setNewsState('active');
setPageNum(1)
loadNews({pageNum, newsState});
};
const incrementPage = () => {
setPageNum(pageNum + 1);
loadNews({pageNum, newsState});
};
useEffect(() => {
locationHashChanged();
loadNews({pageNum, newsState})
.then((response) => {
const {data} = response;
setNews(news.concat(data));
})
.catch((response) => {
console.log(response);
});
}, [pageNum, newsState]);
If I just use incrementPage function - it works fine - it just add more news with previous array of news and update state. But, I see that state is updated, but array is not.
Expected behaviour - when I click on link in Header component (external component), that changes hash in this component ('active' or 'past' fields) and these news should reload with correct fields(active or past). But now I see no changes. How can I fix it? Thanks!
This is my first React project and I would like to use hooks, but I seem to have an issue with an element
function Document(props) {
const [id] = useState(props.match.params.id);
const [document, setDocument] = useState(0);
///////////////////////////////////////
useEffect(function getTheDocument() {
getDocument({
id,
}).then(document => {
setDocument(document.data);
}).catch(error => {
console.log(error);
});
}, [id]);
////////////////////////////////////////////
const [body, setBody] = useState(0);
const [title, setTitle] = useState(0);
////////////////////////
useEffect(function setBodyAndTitle() {
if (document) {
setTitle(document.title);
setBody(document.description);
}
}, [document]);
//////////////////////////
const changeBody = (data) => {
...
const module = ...
setTitle(module[0].title);
setBody(module[0].body);
}
}
So that is how I handle the body and title. When a button is clicked, changeBody is called which finds an object based on some values and sets a new title and body. But that component which is a simple collapsible menu like this. https://www.npmjs.com/package/react-collapse. The button is in the collapsible data. Am I handling this wrong? The menu is no longer collapsible all the hidden button can be seen. I expected the title and body to change ... that's all.
you don't need to handle states of title and body if these are a properties of document object. The same for id, but create an effect to do the request when id change and use state for document by setting a value for this state inside the effect, and create a state for collapse state.
Here is my code to handle this logic
import React from 'react'
function Document({ id = 1 }) {
const [document, setDocument] = React.useState({})
const [innerId, setId] = React.useState(() => id)
const [collapsed, setCollapsed] = React.useState(false)
React.useEffect(() => {
// Call my API request when my ID changes
fetch("https://jsonplaceholder.typicode.com/posts/"+innerId)
.then(response => response.json())
.then(setDocument)
console.log("rerender")
}, [innerId])
// For demo only, increment the ID every 5 seconds
React.useEffect(() => {
const intervalId = setInterval(() =>setId(i => i+1) , 5000)
return () => clearTimeout(intervalId)
}, [])
const toggle = React.useCallback(() => setCollapsed(c => !c))
// You can use useMemo hooks to get title and body if you want
const [title, body] => React.useMemo(
() => [document.title, document.body]
, [document]
)
return (<article>
<header>{document.title}</header>
<hr></hr>
<p style={{height: collapsed ? 0: "100%", overflow: "hidden"}}>
{document.body}
</p>
<button onClick={toggle}>Collapse</button>
</article>
);
}
ReactDOM.render(<Document id={1}/>, document.querySelector("#app"))
This is really cool. I'm beginning to like React. Seems that you can create a
memoized functional component https://logrocket.com/blog/pure-functional-components/ which doesn't update if the props don't change. In my case the props don't change at all after the initial render.
import React, { memo } from 'react';
function PercentageStat({ label, score = 0, total = Math.max(1, score) }) {
return (
<div>
<h6>{ label }</h6>
<span>{ Math.round(score / total * 100) }%</span>
</div>
)
}
function arePropsEqual(prevProps, nextProps) {
return prevProps.label === nextProps.label;
}
// Wrap component using `React.memo()` and pass `arePropsEqual`
export default memo(PercentageStat, arePropsEqual);