skip to Main Content

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


  1. 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:

    const setCountCallback = useCallback((newCount) => setCount(newCount), []);
    

    And then use setCountCallback in your button:

    <button onClick={() => setCountCallback(count + 1)}>Update state {count}</button>
    
    Login or Signup to reply.
  2. 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:

    useEffect(()=>{console.log("component executed and will compare with shadow-dom to check if it needs rerender")});
    useEffect(()=>{console.log("created and rerendered")},[]);
    useEffect(()=>{console.log("created or updated, and rerendered"},[state]);
    

    Without the array, useEffect have nothing to compare, so it will assume that you intentionally need to run this effect with every execution.

    SECOND

    Since ChildMemo don’t have any state and no props are changed, so there is no need to rerender it, so ChildMemo is not executed again.

    But ParentMemo has children 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 pass ChildMemo as it’s as children:

    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>
        </>
      );
    };
    

    and call it from inside ParentMemo:

    function Parent({ children: C }: { children: React.ReactNode | () => React.ReactNode }) {
      useEffect(() => {
        console.log("Parent rerender");
      });
      return (
        <div>
          Parent component
          {typeof C === "function" ? <C /> : C}
        </div>
      );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search