Метод не видит измененный стейт, а лишь его старое состояние

import React, { useCallback, useEffect, useState, useRef } from "react";
import queryString from "query-string";
import { useLocation } from "react-router";
import { getBaseAreasForReports } from "./loaders";

export const TestUseEffect = () => {
  const [pages, setPages] = useState([]);

  const location = useLocation();

  const prevLocation = usePrevious(location);

  const areas2Pages = (areas) => {
    const floors = {};
    areas.forEach((area) => {
      if (floors[area.floor_id]) floors[area.floor_id].push(area);
      else floors[area.floor_id] = [area];
    });
    return Object.entries(floors).map((entry) => entry[1]);
  };

  const batchLoading = useCallback(() => {
    setTimeout(() => console.log('batchLoading', pages), 1000)
  },[pages])

  useEffect(() => {
    const locationChanged = location !== prevLocation
    console.log('location changed', locationChanged)

    if (locationChanged) {
      const query = queryString.parse(location.search)
      getBaseAreasForReports(
        query.continent_id,
        query.country_id,
        query.city_id,
        query.building_id,
        query.floor_id,
        (areas) => {
          const floors = areas2Pages(areas);
          setPages(floors);
          batchLoading();
        }
      );
    }    
  }, [location, batchLoading, prevLocation]);

  useEffect(()=>{
    console.log('pages changed', pages)
  }, [pages])

  return (
    <div>
      Test
      {pages.map((page) => (
        <div key={`page_floor_id_${page[0].floor_id}`}>
          {page.map((area) => (
            <div key={`floor_area_id${area.floor_area_id}`}>
              {area.floor_area_id}
            </div>
          ))}
        </div>
      ))}
    </div>
  );
};

// Hook
function usePrevious(value) {
  // The ref object is a generic container whose current property is mutable ...
  // ... and can hold any value, similar to an instance property on a class
  const ref = useRef();

  // Store current value in ref
  useEffect(() => {
    ref.current = value;
  }, [value]); // Only re-run if value changes

  // Return previous value (happens before update in useEffect above)
  return ref.current;
}

Задача такая: при смене значений в адресной строке (там содержится id) на сервер делается запрос и получает целый список других id (не буду вдаваться в подробности). Далее после успешного получения список парсится (метод areas2Pages) и записывается в стейт pages. Далее другой специальный метод (это должен быть batchLoading) должен запуститься, взять 5 id из списка и пометить их как загружаемые в данный момент и соответственно сделать сразу 5 запросов на сервер. Когда один из запросов получит ответ, то он должен снова вызвать этот же метод, и пометить загруженный id как "загруженный" и найти следующий id который еще не загружен. И т.д. пока не пройдет по списку.

Задача, вроде не сложная. Я взял useEffect и там вызываю запрос к серверу каждый раз, как только изменится адресная строка. Как только ответ получен, я парсю через areas2Pages данные и сохраняю в pages, а далее вызываю тот метод batchLoading, чтобы он начал проход по списку. И бы мог ему сразу передать распарсенный floors, который по сути и сохранялся в стейт только-что. Но, замысел в том, чтобы метод batchLoading сам брал из стейта данные. Но проблема в том, что он не видит измененный стейт. Да, сначала я первым делом подумал - скорее всего он не видит, потому-что стейт еще не успел обновиться. Тогда я сделал таймаут в 0 секунд. Это срабатывало раньше в таких ситуациях. Но тут не сработало. Тогда я задал больше время, но тоже не сработало. Я даже попробовал сделать таймаут прямо после сохранения стейта и распечатать pages прямо там.

  useEffect(() => {
    const locationChanged = location !== prevLocation;
    console.log("location changed", locationChanged);

    if (locationChanged) {
      const query = queryString.parse(location.search);
      getBaseAreasForReports(
        query.continent_id,
        query.country_id,
        query.city_id,
        query.building_id,
        query.floor_id,
        (areas) => {
          const floors = areas2Pages(areas);
          setPages(floors);
          setTimeout(()=>console.log('pages', pages), 0)
          //batchLoading();
        }
      );
    }
  }, [location, pages, prevLocation]);

И даже передал в зависимости pages. Но и это не сработало. Я создал еще один useEffect для pages, чтобы распечатать их при любых изменениях в этом стейте. И они срабатывают до того как распечатываются pages по таймауту - т.е. теоретически к тому времени как таймаут сработал, этот стейт уже поменялся, но тем не менее в логе пустой массив.

Вот лог:

введите сюда описание изображения

Подскажите решение, как заставить метод batchLoading видеть обновленный стейт pages?

UPD: пример кода в сэндбоксе


Ответы (1 шт):

Автор решения: Lev Shportak

Может просто перенести вызов?

useEffect(() => {
    const locationChanged = location !== prevLocation
    console.log('location changed', locationChanged)

    if (locationChanged) {
      const query = queryString.parse(location.search)
      getBaseAreasForReports(
        query.continent_id,
        query.country_id,
        query.city_id,
        query.building_id,
        query.floor_id,
        (areas) => {
          const floors = areas2Pages(areas);
          setPages(floors);
          batchLoading();
        }
      );
    }    
  }, [location, batchLoading, prevLocation]);

  useEffect(()=>{
    //почему не перенести сюда? pages обновится и ваша функция сработает и без 
    //useCallback
    batchLoading();
    console.log('pages changed', pages)
  }, [pages])
→ Ссылка