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]
});