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?

3
  • 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? Commented Sep 13, 2022 at 0: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. Commented Sep 13, 2022 at 0: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 Commented Sep 13, 2022 at 0:40

1 Answer 1

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);
Sign up to request clarification or add additional context in comments.

5 Comments

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?
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
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
... 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.
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.