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