5

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

Jeaf Gilbert
  • 11,495
  • 19
  • 78
  • 105
  • 2
    What causes the parent element re-render? Could you provide a minimal reproducible example via Codesandbox? – dongnhan Nov 15 '20 at 06:15
  • @dongnhan Parent is expected to be rerendered every time I call addToCart function (of CartContext) in a Product component. – Jeaf Gilbert Nov 15 '20 at 07:11
  • You can also pass second callback to compare when to `render` in [React.memo](https://reactjs.org/docs/react-api.html#reactmemo). It would be great if you add sandbox. Otherwise its hard to tell from this – Shubham Verma Nov 15 '20 at 07:24
  • Here we go: https://codesandbox.io/s/dazzling-moore-po1c6?file=/src/App.js – Jeaf Gilbert Nov 15 '20 at 07:58

5 Answers5

3

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

dongnhan
  • 1,698
  • 9
  • 12
  • Ahh, okay. Thank you, dongnhan! It is so neat and works like a charm ✨ – Jeaf Gilbert Nov 15 '20 at 09:52
  • Btw, when we need dynamic qty as onPress param, it breaks the memoization and the issue comes back: https://codesandbox.io/s/eloquent-albattani-8x7h9?file=/src/App.js. Any idea to solve this without 'fast-memoize' plugin? (https://stackoverflow.com/questions/61255053/react-usecallback-with-parameter) – Jeaf Gilbert Nov 15 '20 at 17:34
  • 1
    @JeafGilbert, change this line into this `onAddToCart={handleAddToCart}`. By using `onAddToCart={(addQty)=>{handleAddToCart(addQty)}`, you are creating new a handler everytime which causes the re-renders. – dongnhan Nov 16 '20 at 00:36
  • 1
    Solved! https://codesandbox.io/s/eloquent-albattani-8x7h9?file=/src/App.js – Jeaf Gilbert Nov 18 '20 at 19:13
2

Wrap BestSellers component with React.memo too. Don't use useMemo to avoid unnecessary component updating because it may cause bugs. It is used for computing expensive values.

Source: https://reactjs.org/docs/hooks-reference.html#usememo

QuazBuzz
  • 1,112
  • 1
  • 8
  • 18
  • I think this is also correct! I combined with dongnhan answer as my version of answer (provided in edited question). I really appreciate it, thank you. – Jeaf Gilbert Nov 18 '20 at 19:34
2

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;
Staxlinou
  • 1,351
  • 1
  • 5
  • 20
Basit Ali
  • 37
  • 1
0

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}
//  ...
)
Staxlinou
  • 1,351
  • 1
  • 5
  • 20
Nooruddin Lakhani
  • 7,507
  • 2
  • 19
  • 39
0

The thing is you are using dynamic index for key . when you used dynamic key always react re render this .So use product id or some unique key for this then problem will be solved . I also have same problem and i resolved it

Sachinda Nirmal
  • 2,133
  • 1
  • 8
  • 9