Prevent Child Rerendering if Parent is Rerendered Using Hooks
Asked Answered
L

5

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

Levinson answered 15/11, 2020 at 6:9 Comment(4)
What causes the parent element re-render? Could you provide a minimal reproducible example via Codesandbox?Upsweep
@Upsweep Parent is expected to be rerendered every time I call addToCart function (of CartContext) in a Product component.Levinson
You can also pass second callback to compare when to render in React.memo. It would be great if you add sandbox. Otherwise its hard to tell from thisSensitize
Here we go: codesandbox.io/s/dazzling-moore-po1c6?file=/src/App.jsLevinson
U
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

Upsweep answered 15/11, 2020 at 9:31 Comment(4)
Ahh, okay. Thank you, dongnhan! It is so neat and works like a charm 👍✨Levinson
Btw, when we need dynamic qty as onPress param, it breaks the memoization and the issue comes back: codesandbox.io/s/eloquent-albattani-8x7h9?file=/src/App.js. Any idea to solve this without 'fast-memoize' plugin? (#61255553)Levinson
@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.Upsweep
Solved! codesandbox.io/s/eloquent-albattani-8x7h9?file=/src/App.jsLevinson
C
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

Casting answered 15/11, 2020 at 7:22 Comment(1)
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.Levinson
C
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;
Cubic answered 12/11, 2021 at 19:3 Comment(0)
J
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}
//  ...
)
Jacquard answered 15/11, 2020 at 7:1 Comment(1)
@Jeaf try removing ... from [...new Array(5)]Jacquard
P
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

Perry answered 29/11, 2022 at 3:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.