0

I'm trying to fetch a url when the page first loads and when the location changes. The first load works fine. The record id is pulled from params and when that changes, useEffect gets called again to reset state and reload the new records. However, all of the set* calls in useEffect aren't doing anything as the loadData() method sees the old data still (hasMoreData=false, dataRecords=[...]).

export function MyComponent(props) {
  const [hasMoreData, setHasMoreData] = useState(true);
  const [dataRecords, setDataRecords] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const params = useParams();
  const location = useLocation();
  const { id } = params;

  const loadData = useCallback((page = 1) => {
    if (hasMoreData) {
      fetchSomeData(`api/records?id=${id}&page=${page}`).then((response) => {
        const { data } = response;
        // note: pagination logic omitted for clarity
        if (data && data.length > 0) {
          setDataRecords(data);
        } else {
          setHasMoreData(false);
        }
      });
    }
  }, [id, dataRecords, hasMoreData]);
  
  useEffect(() => {
    // id param has changed, or its the first load - reset all fields, then fetch new data
    setHasMoreData(true);
    setDataRecords([]);
    setCurrentPage(1);
    loadData(currentPage);
  }, [location]);

  // ...currentPage managed via infiniteScroll
};

What am I doing wrong here?

Michael Brown
  • 1,585
  • 1
  • 22
  • 36
  • 1
    What old data is `loadData` seeing? Can you be more specific? – Drew Reese Mar 07 '22 at 23:33
  • fetch is a wrapper that handles most of that stuff and not relevant. Assume fetch returns data. – Michael Brown Mar 07 '22 at 23:48
  • @DrewReese I thought I had distilled it to something simple, but basically there are several pages of data and when the id param changes I need to clear out the previous data and fetch again. However hasMoreData doesn't get set back to true and dataRecords still contains the data from previous fetches despite it being reset in useEffect(). – Michael Brown Mar 07 '22 at 23:52
  • 1
    You are ***enqueueing*** a state update in the `useEffect` hook. React state updates are asynchronously processed. You will still see the state values from the current render cycle when `loadData` is called. – Drew Reese Mar 07 '22 at 23:54
  • @MichaelBrown so you're _not_ using the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API)? – Phil Mar 08 '22 at 00:01
  • 1
    You have a **typo**... `page={page}` should be `page=${page}` (you can even see the syntax highlighting change). FYI this is not a practical use for `useCallback`. – Phil Mar 08 '22 at 00:07
  • @Phil yes it uses the fetchApi within the wrapper, which decomposes the json response and handles JWT authentication details. Thanks for catching the typo - why is it not a practical use for useCallback()? It was the suggested mechanism for code-reuse right in the docs. – Michael Brown Mar 08 '22 at 00:17
  • @MichaelBrown you really only need `useCallback` if you're passing the function to a child component and want to avoid re-renders. See [When to use React.useCallback()](https://aheadcreative.co.uk/articles/when-to-use-react-usecallback/). Regarding `fetch()`, your original question used it directly but now you're using `fetchSomeData()`. Does the latter resolve `response.json()` into a `data` property? – Phil Mar 08 '22 at 00:19
  • I think @DrewReese just hit on the correct issue, since they are async I'm not seeing the state updates when loadData is called. I'm not quite sure what the correct way to handle this is, as the whole example was the result of a bunch of different suggestions accepted on SO. At least this gives me a starting point on implementing it properly – Michael Brown Mar 08 '22 at 00:20
  • @Phil yes, I have no issues with the fetchSomeData wrapper itself, it correctly handles responses and errors asynchronously. There's a lot going on in the actual implementation hence the simple example. I had updated the example to make it clear I wasn't actually calling the fetch api directly. – Michael Brown Mar 08 '22 at 00:22
  • I don't think you need to use `useCallback` at all, you could probably just convert it to an `useEffect` with the same dependencies and move the `loadData` call into it. – Drew Reese Mar 08 '22 at 00:23
  • @DrewReese the loadData method is also called by the infinite scroll pagination on the page to load subsequent pages of data, and not just on page load. That's why it was done this way, for code-reuse of that method which seemed to be the suggested way of going about that. Perhaps I should go back to that and see if I can make it work just based on useEffect() with a dependency on the currentPage value. – Michael Brown Mar 08 '22 at 00:26

1 Answers1

1

Comments on the original question lead me to a better way to achieve what I was after. I needed to detect a location change, pagination changes and loading data on initial page load and useCallback wasn't the best way to go about this because the state changes are asynchronous and it didn't see the new values yet. Instead I used a separate useEffect() for changes to the location so I can reset the internal state data, and a second useEffect() to handle changes to currentPage rather than calling loadData() directly.

export function MyComponent(props) {
  const [hasMoreData, setHasMoreData] = useState(true);
  const [dataRecords, setDataRecords] = useState([]);
  const [currentPage, setCurrentPage] = useState(1);
  const params = useParams();
  const location = useLocation();
  const { id } = params;

  useEffect(() => {
    // location changed, reset all data
    setHasMoreData(true);
    setDataRecords([]);
    setCurrentPage(1);
  }, [location]);
  
  useEffect(() => {
    // id param has changed, or its the first load - reset all fields, then fetch new data
    loadData(currentPage);

    function loadData(page = 1) {
      if (hasMoreData) {
        fetchSomeData(`api/records?id=${id}&page=${page}`).then((response) => {
          const { data } = response;
          // note: pagination logic omitted for clarity
          if (data && data.length > 0) {
            setDataRecords(data);
          } else {
            setHasMoreData(false);
          }
        });
      }
    }
  }, [currentPage]);

  // ...currentPage managed via infiniteScroll
};
Michael Brown
  • 1,585
  • 1
  • 22
  • 36
  • I should note [this answer](https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately) helped me end on this solution. – Michael Brown Mar 08 '22 at 00:45