I have a tabs component that changes state every time a different tab is clicked.
Component Parent
import { useState } from "react";
import "./styles.scss";
import MemoizedTab from "./tab";
export default function App() {
const [selectTab, setSelectTab] = useState("a");
console.log("parent render");
return (
<div className="App">
<div className="tab-list">
<MemoizedTab
tab={"a"}
title={"First Title"}
setSelectTab={setSelectTab}
/>
<MemoizedTab
tab={"b"}
title={"Second Title"}
setSelectTab={setSelectTab}
/>
<MemoizedTab
tab={"c"}
title={"Third Title"}
setSelectTab={setSelectTab}
/>
</div>
{selectTab === "a" && <div>this is a</div>}
{selectTab === "b" && <div>this is b</div>}
{selectTab === "c" && <div>this is c</div>}
</div>
);
}
Component Child
import { memo } from "react";
const MemoizedTab = memo(({ title, tab, setSelectTab }) => {
console.log("child render");
const handleClick = (tab) => {
setSelectTab(tab);
};
return <p onClick={() => handleClick(tab)}>{title}</p>;
});
export default MemoizedTab;
After the initial render the parent has a state of "a". When I click on "First Title" which sets the state to "a" again, nothing renders as expected.
When I first click on "Second Title" which sets the state to "b", I get a console log of "parent renders", which is as expected. But when I click on "Second Title" for the second time, I get a console log of "parent renders" again even though the state didn't change. Nothing logs on the third or fourth clicks. It's always the second.
This same behavior happens when I click on "Third Title" which sets the state to "c".
Why does my parent re-render on the second click after state transitions?
React is not actually re-rendering your component. You can verify this by moving the console.log inside the componentDidUpdate hook.
useEffect(()=>{
console.log('Parent re-rendered!')
})
This console.log won't get logged by the second time you click on any of the tabs.
While the console.log in your provided example does get printed, React eventually bails out of the state update. This is actually an expected behaviour in React. The following extract is from the docs:
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.
This is an interesting one - I did some digging and found the following related React issues:
This is a known quirk due to the implementation details of concurrency in React. We don't cheaply know which of two versions is currently committed. When this ambiguity happens we have to over render once and after that we know that both versions are the same and it doesn't matter.
useState not bailing out when state does not change
This describes the same underlying concept - setting state to the same value may result in the functional component to running again. The key takeaway from them is the last comment from Dan Abramov on the second issue:
React does not offer a strong guarantee about when it invokes render functions. Render functions are assumed to be pure and there should be absolutely no difference for correctness regardless of how many times they're called. If calling it an extra time causes a bug, it's a problem with the code that needs to be fixed.
From the performance point of view, the cost of re-running a single function is usually negligible. If it's not, you should add some useMemos to the parts that are expensive. React does guarantee that if a bailout happens, it will not go updating child components. Even though it might in some cases re-run the component function itself.
So React doesn't gaurntee when it will invoke the functional component, and in this case it appears to run it the additional time checking that the result is the same.
The good news is that although it runs the function an additional time, from what I can tell it doesn't appear to actually rerender on the second invocation - if you use the React profiler, looking at the Flamegraph you can see the first time App renders due to the hook change:
