Search code examples
javascriptreactjsnext.jsfetch

react setState not retaining previous state


I am trying to build an infinite scroll loader component, but somehow my 'items' array resets to [] (emptying) instead of appending the new results as I would expect.

A couple of things to note:

  1. I see the API calls in the network tab when the observer comes into view.
  2. the data returned by the API is fresh and unique (no cache issues)

This is the relevant line: setItems([...items, ...data.data]); // items is resetting to [], data.data is always a new set of fresh values

"use client";
import { useState, useEffect, useRef } from "react";

export function HBSInfiniteScrollList({ request = {} }) {
  const [items, setItems] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const observerTarget = useRef(null);

  const fetchData = async () => {
    setLoading(true);
    try {
      const res = await fetch(request.url, request.options);
      if (res.ok) {
        const data = await res.json();        
        setItems([...items, ...data.data]);
      } else {
      }
    } catch (err) {
      console.log(err);
      setError(err);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          console.log("isIntersecting, fetching new data...");
          fetchData();
        }
      },
      { threshold: 1 }
    );

    if (observerTarget.current) {
      observer.observe(observerTarget.current);
    }

    return () => {
      if (observerTarget.current) {
        observer.unobserve(observerTarget.current);
      }
    };
  }, [observerTarget]);

  return (
    <div>
      <ul className="list-group">
        {items.map((item) => (
          <li key={item.id} className="list-group-item">
            {item.name}
          </li>
        ))}
      </ul>
      {loading && <p>Loading...</p>}
      {error && <p>Error: {error.message}</p>}
      length: {items.length}
      <div ref={observerTarget}></div>
    </div>
  );
}

Anyone see anything wrong with it? I have written this type of code a million times. Maybe I am just tired.


Solution

  • Due to the asynchronous nature of useState, if fetchData is called multiple times in quick succession, there's a chance that items might not have the latest value at the time of each update. This is because state updates may not have been applied yet, leading to overwriting previous updates.

    To ensure that you always have the most recent state when updating, you should use a functional update with setItems. This guarantees that you are using the most up-to-date value of the state.

    You would have to modify the line of code where you update the state of items to this:

    setItems(prevItems => [...prevItems, ...data.data]);

    prevItems is a parameter provided by React's setState function that automatically holds the most current state at the time the update is being applied.

    This should ensure that you are always using the most recent state when updating.