Prevent Child Rerendering if Parent is Rerendered Using Hooks

Viewed 9920

My bestSellerDummy data doesn't change, so I'd like to prevent the same Product child to be rerendered if parent rerenders. I have tried using useMemo in parent and React.memo in child but no luck, it's still showing log 'Rendering Product component..' every time parent rerenders. What am I missing here? Please advice.

Note: Parent is expected to be rerendered every time I call addToCart function (of CartContext) in a Product component.

I'm using CartContext, maybe related to this, I'm not sure. Here is the sandbox: https://codesandbox.io/s/dazzling-moore-po1c6?file=/src/App.js

Home.tsx

const [bestSellerDummy] = useState(
  [...new Array(5)].map((item, key) => ({
    id: key,
    imageUri:'https://1.jpg',
    name: 'My Dummy 1',
    price: 25,
  })),
);

const bestSellers = useMemo(() => {
  return bestSellerDummy.map((productDummy, key) => {
    return (
      <Product key={key} product={productDummy} />
    );
  });
}, [bestSellerDummy]);

return (
  ...
  {bestSellers}
  ...
)

Product.tsx

const Product: FunctionComponent<IProductProps> = (
  productProps,
) => {
  ...
  console.log('Rendering Product component..');
  ...
}

export default React.memo(Product);

=== EDIT: MY VERSION OF ANSWER ===

Finally! After playing around with useCallback, useMemo, fast-memoize plugin.. What suits the best for me is using useReducer in Context combine with wrapping the expensive component with React.memo. I think this is the most clean and elegant way to optimize child components. Working sandbox is here: https://codesandbox.io/s/eloquent-albattani-8x7h9?file=/src/App.js

4 Answers

Since you are using useContext, your component will always re-renders.

When the nearest <MyContext.Provider> above the component updates, this Hook will trigger a rerender with the latest context value passed to that MyContext provider. Even if an ancestor uses React.memo or shouldComponentUpdate, a rerender will still happen starting at the component itself using useContext.

Reference: https://reactjs.org/docs/hooks-reference.html#usecontext

I was trying to refactor your code using the 2nd strategy pointed from the docs: https://github.com/facebook/react/issues/15156#issuecomment-474590693.

However, I soon realized that the addToCart function has cartItems as its dependency, so whenever cartItems changes, addToCart changes and it's kind of impossible to avoid re-renders since every Product component use addToCart function.

That leads me to the use of useReducer because React guarantees that its dispatch is stable and won't change during re-renders.

So here's the working Codesandbox: https://codesandbox.io/s/red-feather-dc7x6?file=/src/App.js:786-797

This is the best way to clear your concepts about useCallback, useMemo and useEffect.

App.js

import Child1 from "./Child1";
import Child2 from "./Child2";
import { useState, useEffect, useMemo, useCallback } from "react";
function App() {
  const [x, setX] = useState(0);
  const [y, setY] = useState(0);
  console.log("Parent");
  const printx = useCallback(() => {
    console.log("x:" + x);
  }, [x]);
  useEffect(() => {
    printx();
    console.log("-------");
  }, [printx]);
  const child1 = useMemo(() => {
    return <Child1 x={x} />;
  }, [x]);
  const child2 = useMemo(() => {
    return <Child2 y={y} />;
  }, [y]);
  return (
    <div className="App">
      <h1>Parent</h1>
      <button onClick={() => setX(x + 1)}>X+</button>
      <button onClick={() => setY(y + 1)}>Y+</button>
      {child1}
      {child2}
    </div>
  );
}

export default App;

Child1.js

const Child1 = ({ x }) => {
  console.log("Child1");
  return (
    <div>
      <h1>Child 1:{x}</h1>
    </div>
  );
};
export default Child1;

Child2.js

const Child2 = ({ y }) => {
  console.log("Child2");
  return (
    <div>
      <h1>Child 2:{y}</h1>
    </div>
  );
};
export default Child2;

Try this way

const [bestSellerDummy, setBestSellerDummy] = useState([]); // default empty

// get data from `useCallback`
const sellerData = React.useCallback(
  () => {
    return [...new Array(5)].map((item, key) => ({
     id: key,
     imageUri:'https://1.jpg',
     name: 'My Dummy 1',
     price: 25,
  }))

  }, []
);

useEffect( () => {
  setBestSellerDummy( sellerData() ); // set data when screen rendered from `useCallback`
}, [])

const bestSellers = useMemo(() => {
// ....
}, [bestSellerDummy]);
 return (
//  ...
{bestSellers}
//  ...
)
Related