Why do state changes before mount cause function execution but don't show on screen?

Viewed 105

Prelude

I know that rendering state changes right after the component mounts should be put into a useEffect hook with an empty dependency array to be rendered, for example:

const [value, setValue] = useState("one");

useEffect(() => {
  setValue("two");
}, []);

Scenario

Out of curiosity I created a component (i.e. this CodeSandbox) with a global variable isFirstRender which checks whether it's the first render to change state for the initial mount of the component:

let isFirstRender = true;

export default function App() {
  const [value, setValue] = useState("one");

  // should only run once at the beginning
  if (isFirstRender) {
    console.log("inside if clause");

    setValue("two");

    isFirstRender = false;
  }

  useEffect(() => {
    console.log("mounted");
  }, []);

  console.log("-- render --", { value });

  return (
    <div className="App">
      <h1>Conditional useState setters</h1>
      <p>Value: {value}</p>
    </div>
  );
}

Stack Snippet:

const {useState, useEffect} = React;

let isFirstRender = true;

/*export default*/ function App() {
    const [value, setValue] = useState("one");
  
    // should only run once at the beginning
    if (isFirstRender) {
        console.log("inside if clause");
    
        setValue("two");
    
        isFirstRender = false;
    }
  
    useEffect(() => {
        console.log("mounted");
    }, []);
  
    console.log("-- render --", { value });
  
    return (
        <div className="App">
            <h1>Conditional useState setters</h1>
            <p>Value: {value}</p>
        </div>
    );
}

ReactDOM.render(<React.StrictMode><App /></React.StrictMode>, document.getElementById("root"));
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

To my surprise the component function gets invoked and the changed value ("two" instead of "one") is visible in the console, but won't render to the screen. It still says "one" in the browser.

Questions

  1. Doesn't the fact that the function is being invoked by the call of the setter function of the useState hook mean that the function "does" render with the updated state value?

  2. I thought in React an invocation of a function component is synonymous of a render? Is the time before a component gets mounted an exception to this rule?

2 Answers

As jkaczmarkiewicz mentions, it happens because of strict mode, which will invoke the rendering of your component twice in development, but only committing DOM changes once.

The preferred solution is to use useEffect when you want to run something once.

useEffect(() => {
  setValue("two");
}, []);

If that for some reason isn't possible you can implement a run-once behavior with a useRef. Basically you just replace your isFirstRender variable with a useRef and look at the isFirstRender.current value.

// Within the component...

const firstRenderRef = useRef(true);

if (firstRenderRef.current) {
    setValue("two");
    firstRenderRef.current = false;
}
Related