I was debugging a React app and noticed that some of my functions were called multiple times in a way I could not explain.
I initially thought it was some sort of "developer feature" and tried to run a build, and all I could see if that the APIs that should not be called were called once instead of twice:
import { useCallback, useState } from "react";
function App() {
const cities = ["montreal", "london", "shanghai"];
const [city, setCity] = useState(cities[0]);
const getCityParameter = useCallback(
(newCity) => {
console.log("[1] getCityParameter");
console.log(`newCity: ${newCity}`);
console.log(`city: ${city}`);
return (newCity ?? city).toUpperCase();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[city]
);
const [cityParameter, setCityParameter] = useState(getCityParameter());
const handleChange = useCallback(
(event) => {
const newCity = event?.target.value;
console.log("handleCityChange");
console.log(`newCity: ${newCity}`);
if (newCity !== undefined) {
setCity(newCity);
}
const newCityParameter = getCityParameter(newCity);
setCityParameter(newCityParameter);
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[city]
);
return (
<>
<select onChange={handleChange} value={city}>
{cities.map((city) => {
return (
<option value={city} key={city}>
{city}
</option>
);
})}
</select>
<div>{cityParameter}</div>
</>
);
}
export default App;
I created this code sandbox here: https://codesandbox.io/s/restless-butterfly-brh7fk?file=/src/App.js
If you clear the console log, and change the dropdown, you will notice that getCityParameter
is called 3 times when I would expect it to be called once.
This seems to be a pretty low-level React feature and I apologize for the "not-so-small" example - this is the best I could come up with to reproduce the behavior.
Can anyone explain?
CodePudding user response:
In the change handler, first:
const newCityParameter = getCityParameter(newCity);
So that's one call for getCityParameter
. Then, the component re-renders because the state setter was called. This:
const [cityParameter, setCityParameter] = useState(getCityParameter());
is like doing
const result = getCityParameter();
const [cityParameter, setCityParameter] = useState(result);
The function gets called again every time the component renders, so you see it again. Finally, because you're in strict mode:
root.render(
<StrictMode>
<App />
</StrictMode>
);
The app re-renders a second time, so getCityParameter
runs again, making a total of 3 times that it's been called when the dropdown is changed.
The initial state value is only used when the component mounts, of course - which means that calling a function every time the component renders when not needed might be seen as unnecessary or confusing. If you wanted getCityParameter
to not be called on re-renders, and to only be called on mount in order to determine the initial state value, use the functional version of useState
. Change
const [cityParameter, setCityParameter] = useState(getCityParameter());
to
const [cityParameter, setCityParameter] = useState(getCityParameter);