2

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?

Nicolas Bouvrette
  • 4,295
  • 1
  • 39
  • 53
  • I am curious why you are setting a useState var to a function instead of a value then using a useEffect to populate and update that value? – Chris Sep 13 '22 at 00:30
  • @Chris OP's approach is one I like to follow too - it feels less clunky to me when I can save the new state in a variable and use that variable instead of an additional `useEffect` (which may not be close, code-distance wise, to the location of the state setter call - making the flow harder to understand at a glance). Matter of opinion, I guess. – CertainPerformance Sep 13 '22 at 00:34
  • :) Nothing but respect! I just had never seen an approach like this one yet, so was just curious of the value. Thank you for explaining @CertainPerformance – Chris Sep 13 '22 at 00:40

1 Answers1

3

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);
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Thanks, but if you clear the console, and only do 1 dropdown change, normally `handleChange` is called - I am not sure I understand why `getCityParameter` would be called multiple times in this scenario? – Nicolas Bouvrette Sep 13 '22 at 00:43
  • When the change is handled, you call a state setter. When the state setter is called, the component re-renders. When the component re-renders, `getCityParameter` is called. Because you're in strict mode, the component re-renders twice - adding up to 3, like it says in the answer – CertainPerformance Sep 13 '22 at 00:44
  • I think the part I'm having difficulty following is why is `getCityParameter` called during re-rederring. Because it's only supposed to be called during the initial `useState` (on page load) and once during `handleChange` – Nicolas Bouvrette Sep 13 '22 at 00:46
  • `... useState(getCityParameter())` is equivalent to `const result = getCityParameter(); ...useState(result)` - parentheses after a function name invokes the function. So every time the component re-renders, the function is invoked. Use lazy initialization (see link in answer) if you only want to call the function the first time the component renders. – CertainPerformance Sep 13 '22 at 00:48
  • Ah! super, thanks now it clarifies - I thought that `useState` was only used at page load but it's because I was using it with a function call. – Nicolas Bouvrette Sep 13 '22 at 00:50