Gopal
Gopal

Reputation: 467

Why callbacks in react functional component is not reading the updated state value

I am trying to implement infinite scroller using intersection observer in react but the problem i am facing is that in the callback of intersection observer i am not able to read latest value of current 'page' and the 'list' so that i can fetch data for next page.

import ReactDOM from "react-dom";
import "./styles.css";
require("intersection-observer");

const pageSize = 30;
const threshold = 5;

const generateList = (page, size) => {
  let arr = [];
  for (let i = 1; i <= size; i++) {
    arr.push(`${(page - 1) * size + i}`);
  }

  return arr;
};

const fetchList = page => {
  return new Promise(resolve => {
    setTimeout(() => {
      return resolve(generateList(page, pageSize));
    }, 1000);
  });
};

let options = {
  root: null,
  threshold: 0
};

function App() {
  const [page, setPage] = useState(1);
  const [fetching, setFetching] = useState(false);
  const [list, setlist] = useState(generateList(page, pageSize));

  const callback = entries => {
    if (entries[0].isIntersecting) {
      observerRef.current.unobserve(
        document.getElementById(`item_${list.length - threshold}`)
      );
      setFetching(true);
/* at this point neither the 'page' is latest nor the 'list'
*they both have the initial states.
*/
      fetchList(page + 1).then(res => {
        setFetching(false);
        setPage(page + 1);
        setlist([...list, ...res]);
      });
    }
  };

  const observerRef = useRef(new IntersectionObserver(callback, options));

  useEffect(() => {
    if (observerRef.current) {
      observerRef.current.observe(
        document.getElementById(`item_${list.length - threshold}`)
      );
    }
  }, [list]);

  return (
    <div className="App">
      {list.map(l => (
        <p key={l} id={`item_${l}`}>
          {l}
        </p>
      ))}
      {fetching && <p>loading...</p>}
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

current behaviour: value of 'page' and 'list' is always equals to the initial state and not the latest value. Infinite scroll is not working after page 2

expected behaviour: In callback function it should read updated value of state 'page' and 'list'.

Here is the working sandbox of this demo https://codesandbox.io/s/sweet-sun-rbcml?fontsize=14&hidenavigation=1&theme=dark

Upvotes: 2

Views: 1810

Answers (3)

Dennis Vash
Dennis Vash

Reputation: 53984

There are primary two problems here, closures and querying the DOM directly.

To solve the closures problem, use functional useState and references:

const listLengthRef = useRef(list.length);
const pageRef = useRef(page);

const callback = useCallback(entries => {
  if (entries[0].isIntersecting) {
    observerRef.current.unobserve(
      document.getElementById(`item_${listLengthRef.current - threshold}`)
    );
    setFetching(true);
    fetchList(pageRef.current + 1).then(res => {
      setFetching(false);
      setPage(page => page + 1);
      setlist(list => [...list, ...res]);
    });
  }
}, []);

const observerRef = useRef(new IntersectionObserver(callback, options));

useEffect(() => {
  listLengthRef.current = list.length;
}, [list]);

useEffect(() => {
  pageRef.current = page;
}, [page]);

Although this code works, you should replace document.getElementById with reference, in this case, it will be a reference to the last element of the page.

Edit frosty-smoke-if4il

Upvotes: 3

duc mai
duc mai

Reputation: 1422

I think the problem is due to the ref keeps reference to the old observer. you need to refresh observer everytime your dependencies gets updated. it relates to closure in js. I would update your app to move the callback inside useEffect

function App() {
  const [page, setPage] = useState(1);
  const [fetching, setFetching] = useState(false);
  const [list, setlist] = useState(generateList(page, pageSize));


  const observerRef = useRef(null);

  useEffect(() => {
    const callback = entries => {
      if (entries[0].isIntersecting) {
        observerRef.current.unobserve(
          document.getElementById(`item_${list.length - threshold}`)
        );
        setFetching(true);
    /* at this point neither the 'page' is latest nor the 'list'
     *they both have the initial states.
     */
        console.log(page, list);
        fetchList(page + 1).then(res => {
          setFetching(false);
          setPage(page + 1);
          setlist([...list, ...res]);
        });
      }
    };
    observerRef.current = new IntersectionObserver(callback, options);

    if (observerRef.current) {
      observerRef.current.observe(
        document.getElementById(`item_${list.length - threshold}`)
      );    
    }
  }, [list]);

  return (
    <div className="App">
      {list.map(l => (
        <p key={l} id={`item_${l}`}>
          {l}
        </p>
      ))}
      {fetching && <p>loading...</p>}
    </div>
  );
}

Upvotes: 0

junwen-k
junwen-k

Reputation: 3664

You can make use of the React setState callback method to guarantee that you will receive the previous value.

Update your callback function as the following and it should work.

const callback = entries => {
  if (entries[0].isIntersecting) {
    setFetching(true);
    setPage(prevPage => {
      fetchList(prevPage + 1).then(res => {
        setFetching(false);
        setlist(prevList => {
          observerRef.current.unobserve(document.getElementById(`item_${prevList.length - threshold}`));
          return ([...prevList, ...res]);
        });
      })
      return prevPage + 1;
    })
  }
};

Upvotes: 1

Related Questions