Use useCallback() and useMemo() efficiently

Functions defined inside function components are recreated each time the component is rendered, resulting in referential inequality. This causes these components to rerender, in some cases unnecessarily.

To prevent rerendering components, you can use the useCallback() and useMemo() hooks. The useCallback() hook returns a memoized callback to maintain referential equality between renders of functions, and the useMemo() hook returns a memoized value to maintain referential equality between renders of values.

Note that useCallback() and useMemo() can result in more memory being allocated, so they must be used appropriately or they may actually reduce performance.

When to use useCallback()

This section describes situations where it is desirable to use useCallback():

  • Avoiding rerendering a child component when a function is recreated.
  • Preventing useEffect() from creating an infinite loop.

Avoid rerendering a child component when a function is recreated

If you have a parent component that passes a callback function to a child component that uses React.memo(), rerendering the parent component recreates the function, which forces the child component to rerender, despite it using React.memo(). To avoid rerendering the child component, wrap the function with useCallback(). For example:

function Parent() {
 const currentDate = 10/10/2020; 
 const someFn = () => {
     ...
  };

  return(
    <Child someFn={someFn} currentValue = {currentDate }/>
  );
}

function Child({ someFn, currentDate }) { ... };
 
 // Wrap function in useCallback() to prevent rerendering the child component 
 const someFn = useCallback(() => {
     ...

  }, [dependencies]);
 
export default React.memo(Child);

Prevent useEffect() from creating an infinite loop

If you invoke a function inside the useEffect() hook, useEffect() expects the function to be declared as a dependency. So each time the function changes, useEffect() runs. But running useEffect() causes the component to recreate the function, which then causes useEffect() to run again, resulting in an infinite loop. For example, the code below can cause an infinite loop:

const getItemFromCart = skuId => {
  const cartItems = Object.values(commerceItems);
  return cartItems.find(cartItem => cartItem.catRefId === skuId);
}

useEffect(() => {
  const selectedCartItem = getItemFromCart(skuId);
  setSomeState(someNewState);
   ...
}, [getItemFromCart]);

To avoid an infinite loop, wrap functions that run inside useEffect() with useCallback(). Wrapping the function in useCallback() ensures the function is not recreated, so useEffect() is not triggered to run again.

When to use useMemo()

If a function includes a complex calculation, you can avoid recomputing it by using the useMemo() hook. This hook returns a memoized value that is not recomputed unless the dependencies change.

In the following example, code that displays images is wrapped in useMemo() to prevent the images from being rerendered whenever the state changes:

<div
  className="ProductImageSlider__Wrapper"
  style={{transform: `translateX(${translateValue}px)`}}
  ref={slideWrapperEl}
  onTouchStart={handleTouchStart}
  onTouchMove={handleTouchMove}
  onClick={() => setPortalRenderedCallback(!portalRendered)}
>
  {useMemo(() => {
    return images.map(image => <Slide key={image} image={image} />);
  }, [images])}
</div>