The question is about why the component renders although the new state equals the previous state (shallow comparison)
// On second button click
const prevState = data
// State trigger
setFoo(data)
// Same state, it doesn't triggers a render
data === prevState
So, the component didn't trigger render due to state change.
But it happened due to another reason, as mentioned in docs "between the lines" under Hooks API Bailing out of a state update section:
Although in some cases React may still need to call your component before skipping the children, it shouldn’t affect your code (react.dev).
Note that React may still need to render that specific component again before bailing out (legacy.reactjs.org).
Unlike in class component, for function components, after setting the same state like in our case, sometimes, React will need another render to validate its equality. Its unfortunate edge case.
But it should not consider you as "performance issue" since it does not effect the React.Node
tree, it won't continue in the reconciliation process if the state didn't change. It even won't make unnecessary hooks calls.
Another Simplified Example
Same goes here, there is another render for bail out, another log of "A".
Although there is a "bail out" render, notice that the useEffect
does not run.
import React, { useEffect } from "react";
import ReactDOM from "react-dom";
const App = () => {
const [state, setState] = React.useState(0);
useEffect(() => {
console.log("B");
});
console.log("A");
return (
<>
<h1>{state}</h1>
<button onClick={() => setState(42)}>Click</button>
</>
);
};
ReactDOM.render(<App />, document.getElementById("root"));

If you wondering on logs order ("Why 'A' logged before 'B'?"), try deep diving another question: React useEffect
in depth / use of useEffect
?