Business
Jobs
  • About Us
  • Solutions
    • Job Postings
      Post your job and receive qualified candidates in 48h.
    • Candidate Assessments
      500+ technical and psychological tests, plus anti-fraud.
    • Headhunting
      Tailor-made executive search from start to finish.
    • Payroll + EOR
      Payroll dispersal and EOR across 15+ LATAM countries.
  • Pricing
  • Jobs

0

152
Views
Why is functional component nested function using a stale state value?

My component fetches and displays posts using infinite scrolling (via an IntersectionObserver). The API call that the component makes is dependent on the current number of fetched posts. This is passed as an offset to the API call.

The desired effect is that, if the posts array is empty, its length is 0 and my API will return the first 5 posts. When the last post in the UI intersects with the viewport, another fetch is made, but this time the API call is passed an offset of 5, so the API skips the first 5 posts in the collection and returns the next 5 instead.

Here is my code:

export default function Feed() {
  const [posts, setPosts] = useState([]);

  const fetchPosts = async () => {
    const newPosts = await postAPI.getFeed(posts.length);
    setPosts((prevPosts) => [...prevPosts, ...newPosts]);
  };

  useEffect(fetchPosts, []);

  const observerRef = useRef(
    new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        observerRef.current.unobserve(entry.target);
        fetchPosts();
      }
    })
  );

  const observePost = useCallback((node) => node && observerRef.current.observe(node), []);

  return (
    <>
      {posts.map((post) => {
        const key = post._id;
        const isLastPost = key === posts.at(-1)._id;
        const callbackRef = !isLastPost ? undefined : observePost; //only if last post in state, watch it!
        
        return <PostCard {...{ key, post, callbackRef }} />;
      })}
    </>
  );
}

However, every time the fetchPosts function is called, the value it uses for posts.length is 0 - the number the function was originally created with.

Can someone explain to me why the closure over posts.length is stale here? I thought that every time a component re-renders, all nested functions within it were recreated from scratch? As such, surely the fetchPosts function should be using the latest value of posts.length every time it is called? Any help is appreciated! :)

about 4 years ago · Juan Pablo Isaza
1 answers
Answer question

0

You are creating a new IntersectionObserver on every render - not a good idea. However, only the first of all these created observers, the one you store in the ref (the ref that is never updated), is the one on which you call observe(), and that first observer uses a stale closure over the initial value of posts.

Instead, I would suggest creating the intersection observer(s) inside the observePost function:

const [posts, setPosts] = useState([]);

const fetchPosts = useCallback(async () => {
  const newPosts = await postAPI.getFeed(posts.length);
  setPosts((prevPosts) => [...prevPosts, ...newPosts]);
}, [posts.length]);
//  ^^^^^^^^^^^^

useEffect(fetchPosts, []);

const observePost = useCallback((node) => {
  if (!node) return;
  const observer = new IntersectionObserver(([entry]) => {
    if (entry.isIntersecting) {
      observer.unobserve(entry.target);
      fetchPosts();
    }
  });
  observer.observe(node);
}, [fetchPosts]);
//  ^^^^^^^^^^

Important here is the dependency of the observePost callback on the fetchPosts callback which has a dependency on the length of posts, to avoid getting stale. It's probably also possible to solve this with refs, but I don't think they're necessary here.

about 4 years ago · Juan Pablo Isaza Report
Answer question
Find remote jobs

Discover the new way to find a job!

Top jobs
Top job categories
Business
Post vacancy Pricing Sales
Legal
Terms and conditions Privacy policy
© 2026 PeakU Inc. All Rights Reserved.
Andres GPT
Show me some job opportunities
There's an error!