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.
>Solution :
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']`