Home > Software design >  React fetch more data and append to state
React fetch more data and append to state

Time:08-05

I have a problem with a classic infinite scroll, when loading more data and appending to the state.

I have a page that displays, among other things, a custom infinite scrollable list of cities.
The cities are fetched from an external library.

import { City, Country, CitiesQuery } from "an-external-library";

export const CountryComponent: FC<Props> = ({ country: Country }) => {
  const [cities, setCities] = useState<City[]>([]);
  const [citiesQuery, setCitiesQuery] = useState<CitiesQuery>();

  const loadMoreCities = useCallback(() => {
    console.log("loadMoreCities", "cities", cities);

    citiesQuery
      ?.load((newCities) => {
        console.log("loadMoreCities", "newCities", newCities);
        const updatedCities = [...cities, ...newCities];
        console.log("loadMoreCities", "updatedCities", updatedCities);
        setCities(updatedCities);
      })
      .then(() => setCitiesQuery(citiesQuery))
      .catch(console.error)
  }, [cities]); // I've tried adding more dependencies here

  useEffect(() => {
    const query = country.createCitiesQuery()
    query.limit = 3; // example
    query
      .load(setCities)
      .then(() => setCitiesQuery(query)) // so that I can load more from where I left
      .catch(console.error)
  }, []);

  useEffect(() => {
    // so that we know what's actually being set
    console.log("cities", cities);
  }, [cities])

  return (
    <>
      // ...
      <CitiesList cities={cities} onScrollBottom={loadMoreCities}/>
    </>
  )
}

My problem is that the loadMoreCities function doesn't seem to be updating when cities changes so, inside loadMoreCities, cities is always the same and doesn't reflect previous appends.

Here's an example run through logs:

// initial render
cities: Array(0) []

// after first useEffect runs
cities: Array(3) ["A", "B", "C"]

// scroll bottom
loadMoreCities, cities: Array(3) ["A", "B", "C"]
loadMoreCities, newCities: Array(3) ["D", "E", "F"]
loadMoreCities, updatedCities: Array(6) ["A", "B", "C", "D", "E", "F"]
cities: Array(6) ["A", "B", "C", "D", "E", "F"]

// scroll bottom again
loadMoreCities, cities: Array(3) ["A", "B", "C"]  // <---- this is a problem
loadMoreCities, newCities: Array(3) ["G", "H", "I"]
loadMoreCities, updatedCities: Array(6) ["A", "B", "C", "G", "H", "I"]
cities: Array(6) ["A", "B", "C", "G", "H", "I"] // <---- as consequence, we lost ["D", "E", "F"]

I've tried adding all possible combinations of dependencies in the useCallback, but nothing seemed to work.
I'm sure I'm missing something really simple, but can't see what.

CodePudding user response:

Solution

Try:

.load((newCities) => {
  setCities(cities => [...citiesm, ...newCities]);
})

Instead of:

.load((newCities) => {
  setCities([...citiesm, ...newCities]);
})

Explanation

setState can take a value or a setter function

Consecutive calls with a value will result in only the last applied

const [state, setState] = useState([])

setState([...state, 'a']) // current state = []
setState([...state, 'a']) // current state = []
setState([...state, 'c']) // current state = []
// the result will be `['c']`

Using setter callback will make react remember each one and then run each with updated data

const [state, setState] = useState([])

setState(state => [...state, 'a']) // current state = []
setState(state => [...state, 'a']) // current state = ['a']
setState(state => [...state, 'c']) // current state = ['a', 'b']
// the result will be `['a', 'b', 'c']`

CodePudding user response:

Try doing this to access directly the current state when updating it :

setCities((currentCities) => {
 return [currentCities, ...newCities]
});
  • Related