简体   繁体   中英

Can you control the re-renders of a functional react component based on state change?

I have a basic e-commerce app for practice. There's a functional component called "Shop" which has two states:

[products, setProducts] = useState([10ProductObjects])
and [cart, setCart] = useState([])

On the first render cycle, the 10 products are loaded and each Product component has an "add to cart" button that adds a product to the cart array. When I click the button, the cart array gets populated and its length is displayed. However, doing so re-renders the 10 products again even though nothing has been changed on them.

Now as I see it since one of the states changes ie the cart array, the whole Shop component is rendered again. Which in turn renders its child components, including those which were not changed.

I've tried to use React.memo but it doesn't help since no props are being changed on "Shop", rather the state is changing. I've also used the useMemo hook and it had some interesting results.

Using the products array as a dependency solves the extra re-rendering problem, but the new products are not added to the cart anymore. Using both [products, cart] as the dependencies works but brings back the original problem.

I know it could be done using shouldComponentUpdate but I need that kind of flexibility in functional components.

NB: This is my first ever question and I'm all ears for any kind of feedback.

import React, { useState, useMemo } from 'react';
import fakeData from '../../fakeData';
import Product from '../Product/Product';

const Shop = () => {
console.log('[Shop.js] rendered')
const first10 = fakeData.slice(0, 10);
const [products, setProducts] = useState(first10);
const [cart, setCart] = useState([]);


const addProductHandler = (product) => {
    console.log(cart, product);
    const newCart = [...cart, product];
    console.log(newCart); 
    setCart(newCart);
    
}
let productsOnScreen = useMemo(() => {
    return products.map( prod => {
        return  <Product product={prod} addProductHandler={addProductHandler} />
    });
}, [products])

 
return (
    <div className="shop-container">
        <div className="product-container">
            {productsOnScreen}
        </div>
        <div className="cart-container">
            <h3>this is cart</h3>
            <h5>Order Summary: {cart.length}</h5>
    </div>
    </div>
);
};

export default Shop;

Memoize addProductHandler withReact.useCallback so that the reference to it does not change between renders:

const addProductHandler = React.useCallback((product) => {
    setCart(oldCart => {
        return [...oldCart, product];
    });
}, [setCart]);

Then, memoize <Product> with React.memo . You didn't post your code for that component but it would look something like this:

export const Product = React.memo((props) => {
  // normal functional component stuff

  return <>product component or whatever</>;
});

These are both necessary for a component to avoid unnecessary rerenders. Why?

  1. React.useCallback allows for comparison-by-reference to work for a callback function between renders. This does not work if you declare the callback function every render, as you have currently.
  2. React.memo wraps a component to enable it to render or not render depending on a shallow comparison of its props. React components do NOT do this by default, you have to explicitly enable it with React.memo .

As the docs mention:

This method only exists as a performance optimization. Do not rely on it to “prevent” a render, as this can lead to bugs.

In other words, you should only use this as a performance optimization if you are experiencing slowdowns, NOT to prevent renders for any other reason, and ONLY if you are actually experiencing performance issues.

If this doesn't work, it's likely that one of your <Product> props is still changing (in the context of shallow comparison-by-reference) between renders. Keep playing with and memoizing props until you figure out which ones are changing between renders. One way to test this is a simple React.useEffect (for debug purposes only) inside of <Product> which will alert you when a prop has changed:

React.useEffect(() => {
    console.log('product prop changed');
}, [product]);

React.useEffect(() => {
    console.log('addProductHandler prop changed');
}, [addProductHandler]);

// etc...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM