Home > database >  updating a useState to the value that it's already holding inside of a custom React hook causes
updating a useState to the value that it's already holding inside of a custom React hook causes

Time:02-18

function useHandleURL(mode, page) {
  const [is_page_hidden, set_is_page_hidden] = useState(true);

  ...

  set_is_page_hidden(true);
}

The above will cause an infinite re-render.

I had to solve by doing this:

function useHandleURL(mode, page) {
  const [is_page_hidden, set_is_page_hidden] = useState(true);

  ...

  if (!is_page_hidden) {
    set_is_page_hidden(true);
  }
}

This is not the behavior inside of React components. Inside a component, if I set a useState to true when it is already true, then it will not cause re-render.

Can someone confirm this behavior and explain why it causes infinite re-render inside of a Hook but not a Component?

CodePudding user response:

I can confirm that using the same exact code within the body of a function component render loops the same as when it's in a custom hook. The issue is the unconditional calling of the state setter.

See useState Bailing out of a state update

If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. (React uses the Object.is comparison algorithm.)

Note that React may still need to render that specific component again before bailing out. That shouldn’t be a concern because React won’t unnecessarily go “deeper” into the tree. If you’re doing expensive calculations while rendering, you can optimize them with useMemo.

Also note that "React may still need to render that specific component again before bailing out." means running the render function one more time, not "render to the DOM one more time", so any unexpected side-effects, like enqueueing another state update are problematic. The entire function body of a function component is the render function.

Consider the following code though:

function App() {
  const [is_page_hidden, set_is_page_hidden] = React.useState(true);

  const handler = () => set_is_page_hidden(true);

  React.useEffect(() => {
    console.log("RENDERED!");
  });
  
  return <button type="button" onClick={handler}>Click</button>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  rootElement
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="root" />

We're conditionally enqueueing state updates with the same value and notice that no rerenders are triggered as measured by the useEffect hook logging 1 effect per 1 render cycle.

Conclusion

Conditionally enqueueing the state update is correct.

function useHandleURL(mode, page) {
  const [is_page_hidden, set_is_page_hidden] = useState(true);

  ...

  if (!is_page_hidden) {
    set_is_page_hidden(true);
  }
}

Update

I just realized that it's not necessarily the unconditional state update, but more the unintentional side-effect.

  • Render loops

     function App() {
       const [is_page_hidden, set_is_page_hidden] = React.useState(true);
    
       set_is_page_hidden(true);
    
       return ...;
     }
    
  • Stable, no render looping

        function App() {
          const [is_page_hidden, set_is_page_hidden] = React.useState(true);
    
          React.useEffect(() => {
            console.log("RENDERED!");
            set_is_page_hidden(true);
          });
      
          return "Stackoverflow is awesome.";
        }
    
        const rootElement = document.getElementById("root");
        ReactDOM.render(
          <React.StrictMode>
            <App />
          </React.StrictMode>,
          rootElement
        );
        <script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
        <div id="root" />

In the stable version the state update is an intentional state update as an intentional side-effect, so no rerenders are triggered since the state value is the same as the previous render cycle.

  • Related