skip to Main Content

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


  1. The way you wrote your code, the getAllPosts() function is created anew every time your component is rendered. And the handleScroll() function is created when useEffect(), 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

    Login or Signup to reply.
  2. 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?

    This is a Javascript Closure.

    A closure is the combination of a function bundled together
    (enclosed) with references to its surrounding state (the lexical
    environment
    ). In other words, a closure gives you access to an outer
    function’s scope from an inner function. In JavaScript, closures are
    created every time a function is created, at function creation time.

    You close over a copy of the arePostsLoading state in the loadMorePosts callback function that is closed over in the handleScroll 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 of loadMorePosts is created that closes over the current arePostsLoading value, which is closed over in a new copy of handleScroll and passed as the scroll event listener.

    Suggestion(s)

    If loadMorePosts is used elsewhere in the component then you should memoize the function with useCallback and specify arePostsLoading and the page values as dependencies so they are properly enclosed and include loadMorePosts in the useEffect hook’s dependency.

    const [arePostsLoading, setArePostsLoading] = useState(true);
    
    const loadMorePosts = React.useCallback(() => {
      const nextPageNo = pageData.pageNo + 1;
      if (arePostsLoading) {
        getAllPosts(nextPageNo, pageSize);
      }
    }, [arePostsLoading, /* pageData, 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
          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);
      };
    }, [loadMorePosts]);
    

    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:

    const [arePostsLoading, setArePostsLoading] = useState(true);
    const arePostsLoadingRef = React.useRef(arePostsLoading);
    
    useEffect(() => {
      arePostsLoadingRef.current = arePostsLoading;
    }, [arePostsLoading]);
    
    const loadMorePosts = () => {
      const nextPageNo = pageData.pageNo + 1;
      if (arePostsLoadingRef.current) {
        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
          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);
      };
    }, []);
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search