skip to Main Content

I was reading React Docs about useCallback and memo and got a little confused if I had to use both at the same time in the example below from their docs.

Starter code:

function ProductPage({ productId, referrer, theme }) {
  // ...
  return (
    <div className={theme}>
      <ShippingForm onSubmit={handleSubmit} />
    </div>
);

From the code above, toggling the theme prop freezes the app for a moment, but if you remove <ShippingForm /> from the JSX, it feels fast. Therefore, we’ll have to optimize the ShippingForm component because by default, when a component re-renders, React re-renders all of its children recursively. So, to solve the problem we can tell ShippingForm to skip re-rendering when its props are the same as on last render by wrapping it in memo as below:

import { memo } from 'react';

    const ShippingForm = memo(function ShippingForm({ onSubmit }) {
      // ...
});

However, if the handleSubmit function is an arrow function or a regular function using the function keyword as below, the memo optimization won’t work because in JavaScript, a
function () {} or () => {} always creates a different function, similar to how the {} object literal always creates a new object. Hence, the ShippingForm props will never be the same, and the memo optimization won’t work.

The code below will not work

function ProductPage({ productId, referrer, theme }) {
  // Every time the theme changes, this will be a different function...
  function handleSubmit(orderDetails) {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }
  
  return (
    <div className={theme}>
      {/* ... so ShippingForm's props will never be the same, and it will re-render every time */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

And this also will not work

 function ProductPage({ productId, referrer, theme }) {
  // Every time the theme changes, this will be a different function...
  const handleSubmit = (orderDetails)=> {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }
  
  return (
    <div className={theme}>
      {/* ... so ShippingForm's props will never be the same, and it will re-render every time */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

So, to solve the issue we’ll have to use the useCallback hook as below.

Now my question is since we’re using the useCallback hook do we still keep telling ShippingForm to skip re-rendering when its props are the same as on last render by wrapping it in memo? Or it’s unnecessary?
Thanks for helping.

function ProductPage({ productId, referrer, theme }) {
  // Tell React to cache your function between re-renders...
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]); // ...so as long as these dependencies don't change...

  return (
    <div className={theme}>
      {/* ...ShippingForm will receive the same props and can skip re-rendering */}
      <ShippingForm onSubmit={handleSubmit} />
    </div>
  );
}

2

Answers


  1. Yes, you should use both.

    P.S.
    You have a little mistake in your question: useMemo is a hook for memoizing values inside component (like useCallback but it saves result of function execution). React.memo is almost same stuff but for Components itself. You are asking about last thing.

    Login or Signup to reply.
  2. All the confusion is coming from the fact you are assuming a component renders when receiving different props. Actually, a component only renders when its internal state changes or when its parent renders. This is why, this Child component with no props, gets called again when its parent renders:

    const Child = () => {
      console.log("Child with no props renders");
      return <div></div>;
    };
    
    const Parent = () => {
      const [state, setState] = React.useState(true);
    
      console.clear();
      console.log("Parent renders");
      
      
      return (
        <div>
          <button onClick={() => setState((prev) => !prev)}>Render parent</button>
          <Child />
        </div>
      );
    }
    
    ReactDOM.render(
      <Parent />,
      document.getElementById("root")
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

    When this behavior is problematic, because Child does lots of computing, for example, you could tell React with the help of memo to render it only when it receives new props:

    const Child = React.memo(() => {
      console.log("Child with no props renders");
      return <div></div>;
    });
    
    const Parent = () => {
      const [state, setState] = React.useState(true);
    
      console.clear();
      console.log("Parent renders");
      
      return (
        <div>
          <button onClick={() => setState((prev) => !prev)}>Render parent</button>
          <Child />
        </div>
      );
    }
    
    ReactDOM.render(
      <Parent />,
      document.getElementById("root")
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

    memo compares the previous props with the new one (while rendering Parent). If it’s the same, it skips rendering Child. This would work whether you are not passing any props like the above or primitive ones. However, if you pass an object or a function defined in the body of Parent, they get passed down with different memory references, hence we use useCallback to memoize functions, and useMemo to memoize objects:

    const ChildWithPrimitiveProp = React.memo(() => {
      console.log("ChildWithPrimitiveProps renders");
      return <div></div>;
    });
    
    const ChildWithNonMemoizedFunctionProp = React.memo(() => {
      console.log("ChildWithNonMemoizedFunctionProp renders");
      return <div></div>;
    });
    
    const ChildWithNonMemoizedObjectProp = React.memo(() => {
      console.log("ChildWithNonMemoizedObjectProp renders");
      return <div></div>;
    });
    
    const ChildWithMemoizedFunctionProp = React.memo(() => {
      console.log("ChildWithMemoizedFunctionProp renders");
      return <div></div>;
    });
    
    const ChildWithMemoizedObjectProp = React.memo(() => {
      console.log("ChildWithMemoizedObjectProp renders");
      return <div></div>;
    });
    
    const Parent = () => {
      const [state, setState] = React.useState(true);
      
      function fn(){};
      const obj = {};
      
      const memoizedFn = React.useCallback(fn, []);
      const memoizedObj = React.useMemo(()=>obj, []);
      
      console.clear();
      console.log("Parent renders");
      
      return (
        <div>
          <button onClick={() => setState((prev) => !prev)}>Render parent</button>
          <ChildWithPrimitiveProp message="Hello World" />
          <ChildWithNonMemoizedFunctionProp fn ={fn}/>
          <ChildWithNonMemoizedObjectProp fn ={obj}/>
          <ChildWithMemoizedFunctionProp fn ={memoizedFn}/>
          <ChildWithMemoizedObjectProp fn ={memoizedObj}/>
        </div>
      );
    }
    
    ReactDOM.render(
      <Parent />,
      document.getElementById("root")
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.0/umd/react.production.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.0/umd/react-dom.production.min.js"></script>
    <div id="root"></div>

    So you need both or one of them depending on your use case.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search