In the below given react.js code, I am fetching some data on page load. I have used arePostsLoading
state variable to track if the data is loaded or not. I have also added a function handleScroll
which tracks scrolling and run another function loadMorePosts
when the page is scrolled to the end of the page. Now I am using console.log to check the value of arePostsLoading
inside the loadMorePosts
function.
Now when the page is loading, arePostsLoading
is initally true. Then the content is loaded and value is set to false. I can confirm the value is set to false because when the value of the variable is false, the content loaded is visible on the page and the loading spinner is hidden. After this, I scroll the page to bottom to trigger the loadMorePosts
function which just console.log’s the arePostsLoading
value(for testing). The logged value I was getting was true
which was not the current value of the arePostsLoading
. This confused me because the current value of arePostsLoading
should be false(which is confirmed by the loaded content and react dev tools).
After a lot of debugging I found out the issue, the function I was adding to the event listener at the start of the page load was containing the old value of arePostsLoading
. I fixed this by adding arePostsLoading
as dependency so that a new event listener is created when the arePostsLoading
value is changed.
The issue is fixed but I can’t understand why It works this way. I am just passing the function to the event listener not the value of the arePostsLoading
, so why is it taking the old value. Also, if I add another function inside loadMorePosts
and then console.log the value will it give the new value(example below: eg)?
Sorry, for the long description of the question. I just wanted it to be very clear to understand.
Code:
const [arePostsLoading, setArePostsLoading] = useState(true);
//load posts
const getAllPosts = async (newPageNo, newPageSize) => {
setArePostsLoading(true);
const [data, error] = await sendRequest(
//...fetch data code
);
if (error) {
console.log(error);
handleError(error);
return;
}
setArePostsLoading(false);
};
useEffect(() => {
// setArePostsLoading(true);
getAllPosts(1, pageSize);
},[]);
//Load more posts
const loadMorePosts = () => {
const nextPageNo = pageData.pageNo + 1;
if (arePostsLoading) {
getAllPosts(nextPageNo, pageSize);
}
};
useEffect(() => {
function handleScroll() {
const scrollHeight = document.documentElement.scrollHeight;
const clientHeight = document.documentElement.clientHeight + 2;
const scrollTop =
window.pageYOffset || document.documentElement.scrollTop;
// Check if the user has scrolled to the bottom of the page
if (scrollHeight - scrollTop < clientHeight) {
// Perform your action here, such as loading more content
console.log("End of page reached!");
loadMorePosts();
}
}
// Attach the scroll event listener to the window object
window.addEventListener("scroll", handleScroll);
// Clean up the event listener when the component is unmounted
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, []);
// }, [arePostsLoading]); // fix added later
/////////////////////
//eg 1 : example code
function loadMorePosts(){
test1();
}
function test1(){
console.log(arePostsLoading);
}
2
Answers
The way you wrote your code, the
getAllPosts()
function is created anew every time your component is rendered. And thehandleScroll()
function is created whenuseEffect()
, and will record the state of the component mount; hence in the handler you will not be able to access the updated state.You should put your window listener callback function in a
useCallback()
hook. Also as you realized there is no reason to put the state variable in the dependencies.See this StackOverflow post for an example
This is a Javascript Closure.
You close over a copy of the
arePostsLoading
state in theloadMorePosts
callback function that is closed over in thehandleScroll
callback function that is passed as the scroll event handler.When you used an empty dependency array you are saying to do this exactly once at the end of the initial render cycle. The
arePostsLoading
value will be whatever it is/was on the initial render cycle and will never be updated.When you add
arePostsLoading
as an external dependency each time that it updates a new copy ofloadMorePosts
is created that closes over the currentarePostsLoading
value, which is closed over in a new copy ofhandleScroll
and passed as the scroll event listener.Suggestion(s)
If
loadMorePosts
is used elsewhere in the component then you should memoize the function withuseCallback
and specifyarePostsLoading
and the page values as dependencies so they are properly enclosed and includeloadMorePosts
in theuseEffect
hook’s dependency.This isn’t much work, but it is some work. If you’d prefer to not tear down and remove event listeners and instantiate new listeners all just to capture the current
arePostsLoading
state value you can cache the state into a React ref where the ref’s current value can be updated and accessed at any time in the future from within the closure. The ref is closed over, but the value can be mutated.Basic Example: