I have page displaying a list of countries provided by an external library. When I click on a country, it shows me all cities in it.
Each country object has a method to get the list of cities in it, provided by the library.
I have a CountryComponent like this
import { City, Country } from "an-external-library";
export const CountryComponent: FC<Props> = ({ country: Country }) => {
const [cities, setCities] = useState<City[]>([]);
useEffect(() => {
country
.getCities()
.then(setCities)
.catch(console.error);
}, [country]);
// ...
}
I want to get cities every time the component mounts and also if the country changes.
The problem is that if I keep changing the country quickly, it will call getCities() every time I click on a country on the list. Since there's no way of knowing in which order the responses to those calls will arrive, in the end cities will be the response that arrived last, whatever it is.
This means I may end up with country: France; cities: New York, Los Angeles, etc...
Solutions I've tried
CountryComponent and mounting again if country changes.In the parent component, I have
const handleChangeCountry = (c: Country) => {
setCountry(undefined);
setTimeout(() => setCountry(c), 10);
}
render (
<>
<CountryList onCountryClick={handleChangeCountry}/>
{country ?
<CountryComponent country={country}/>
:
<Spinner />
})
</>
)
This a solution so ugly that just writing this gives me the creeps.
But it worked.
Just that I couldn't get the UI to look clean when the CountryComponent disappeared for 10 ms and appeared again; and want a better solution.
Solutions I haven't tried
As suggested by @chimera, debouncing could be a solution.
While searching for solutions before posting, I found this answer, but didn't try it because I believe that it would cause the list of cities to take a while to respond to clicks on countries, and that's not ideal either.
Ideally, I would like to ignore all callbacks from getCities() except the last, but I don't know how I'd achieve this.
This is a typical issue: you get unordered asynchronous responses, potentially overwriting the most recent (and the only relevant) state.
A very easy solution is to use react-use's useAsync hook, which includes several useful mechanisms for this typical issue, the one interesting here being that it discards the responses of every previous requests, keeping only the response to the most recent request.
React hook that resolves an async function or a function that returns a promise;
import { useAsync } from "react-use";
const CountryComponent = ({ country: Country }) => {
// With react-use's useAsync,
// no need for separate state and useEffect
const cities = useAsync(
() => country.getCities(),
[country]
).value; // returned object { loading: boolean, value: T, error }
// ...
}