I have two components, one receiving another one as a prop. For clarity I will remove all the unnecessary logic and JSX:
function Child() {
useEffect(() => {
console.log("Child rerender");
});
return <div>Child component</div>;
}
function Parent({ children }: { children: React.ReactNode }) {
useEffect(() => {
console.log("Parent rerender");
});
return (
<div>
Parent component
{children}
</div>
);
}
They’re both memoized as follows:
const ChildMemo = React.memo(Child);
const ParentMemo = React.memo(Parent);
Now, they’re both being rendered in the top level component with a state, again for clarity’s sake let’s imagine for now it’s just a button setting some state:
function App() {
useEffect(() => {
console.log("App rerender");
});
const [count, setCount] = useState(0);
return (
<>
<button onClick={() => setCount(count + 1)}>Update state {count}</button>
<ParentMemo>
<ChildMemo />
</ParentMemo>
</>
);
};
I expect that clicking a button triggers state update leading to just "App rerender" being logged in the console. Yet the <Parent />
component is being rerendered as well. Why does that happen?
2
Answers
This is happening because the setCount function from useState is creating a new reference on every render. This means that even though the count value is the same, the setCount function itself is a new reference.
Since the Parent component is receiving children as a prop, and children includes the ChildMemo component, any change in the reference of ChildMemo will cause the Parent component to re-render, even if the count value hasn’t changed.
To fix this issue, you can use the useCallback hook to memoize the setCount function:
And then use setCountCallback in your button:
FIRST
Looking at your code, you’re using
useEffect
without deps array.So the
parent rerendered
message you see in the console is not because it’s rerendered, but because you didn’t define dependencies array;so every time:
a state changes -> the
ParentMemo
is executed -> the effect will run -> message is printed to console,but it doesn’t mean it’s rerendered.
If you want to know if it’s rerendered, you must add empty dependencies array:
Without the array,
useEffect
have nothing to compare, so it will assume that you intentionally need to run this effect with everyexecution
.SECOND
Since
ChildMemo
don’t have any state and no props are changed, so there is no need to rerender it, soChildMemo
is not executed again.But
ParentMemo
haschildren
prop, so you can’t guarantee it will not be rerendered.To quote from the Docs:
"Wrap a component in memo to get a memoized version of that component. This memoized version of your component will usually not be re-rendered when its parent component is re-rendered as long as its props have not changed. ((But)) React may still re-render it: memoization is a performance optimization, not a guarantee."
To avoid
ParentMemo
re-execution, you can do an ugly solution, which is to passChildMemo
as it’s as children:and call it from inside
ParentMemo
: