Problem Of re-rendring the component and using memo()

Viewed 224

The problem is when I update the quantity of an item the parent component start to re-render again I want to understand what am doing wrong I already using memo to stop doing the re-render so what should i do if i wanna fix the re-render and only update the object on array with out re-rendring, if somebody can help me with that? Thanks in advance

Parent Component

export const ProductsScreen = ({ }) => {
    const [Products, setProducts] = useState([
       // sample of object props
        {
            id: '1',
            name: 'first item',
            volume: 500,
            price: 150.25,
            total: 4,
            qty: 0
        }]


    const renderProducts = ({ item }) => {
        const { qty } = item
        return (
            <ProductListItems
                product={item}
                qty={qty}
                onAddItem={onAddItem}
                onRemoveItem={onRemoveItem} />
        )
    }


    const updateObjectInArray = (newProduct) => {
        return setProducts(
            Products.map((item) => {
                if (item.id !== newProduct.id) {
                    return item
                }
                return {
                    ...item,
                    ...newProduct
                }
            })
        )
    }

    const onAddItem = productId => {
        let product = Products.find(i => {
            return i.id === productId
        })
        const { total, qty } = product
        const newProduct = {
            ...product,
            qty: qty + 1,
            total: total - 1
        }
        updateObjectInArray(newProduct)
    }

    const onRemoveItem = productId => {
        let product = Products.find(i => {
            return i.id === productId
        })
        const { total, qty } = product
        const newProduct = {
            ...product,
            qty: qty - 1,
            total: total + 1
        }
        updateObjectInArray(newProduct)
    }

    return (
        console.log("RERENDERD"),
        <View style={styles.container} >
            <SafeAreaView style={{ flex: 1, }}>
                <FlatList
                    contentContainerStyle={{ paddingVertical: 15 }}
                    showsVerticalScrollIndicator={false}
                    data={Products}
                    renderItem={renderProducts}
                    keyExtractor={item => item.id}
                />
            </SafeAreaView>
        </View >
    )
};

Child Component

const ProductListItems = ({
    product,
    qty,
    onAddItem,
    onRemoveItem
}) => {
    return (
        // design here
    )
};

export default memo(ProductListItems, (prevState, nextState) => prevState.qty === nextState.qty);
2 Answers

The handlers add and remove are only created on mount using React.useCallback because there are no dependencies, using the callback to the state setter to prevent a dependency on products.

The renderItem prop has to be a function that returns jsx but since ProductListItems is a pure component it won't re render as long as props didn't change. Not sure how FlatList works but this should prevent each item from rendering if only one item changed.

const { useState, useCallback, memo } = React;
//just for this demo, naive implementation of FlatList
const FlatList = memo(function FlatList({
  data,
  renderItem,
  keyExtractor,
}) {
  return (
    <ul>
      {data.map((item) => {
        //strange need for renderItem to be a function that
        //  returns jsx but let's roll with it I'm sure there
        //  is a good reason for it
        const content = renderItem({ item });
        return (
          <div key={keyExtractor(item)}>{content}</div>
        );
      })}
    </ul>
  );
});
//Make sure this component is pure so it won't re create jsx
//  even if you didn't use memo the handlers would not cause
//  a DOM re render (not sure about native)
const ProductListItems = memo(function ProductListItems({
  product,
  // removed qty, it is in product
  onAddItem,
  onRemoveItem,
}) {
  console.log(
    'render ProductListItems for id:',
    product.id
  );
  return (
    <li>
      <pre>{JSON.stringify(product, undefined, 2)}</pre>
      <button onClick={() => onAddItem(product.id)}>
        Add
      </button>
      <button onClick={() => onRemoveItem(product.id)}>
        Remove
      </button>
    </li>
  );
});
//this is the code you could use, I removed some jsx but logic
//  should be re usable
const ProductsScreen = () => {
  const [products, setProducts] = useState([
    {
      id: 1,
      total: 4,
      qty: 0,
    },
    {
      id: 2,
      total: 6,
      qty: 0,
    },
  ]);
  //when products change this render is
  //  needed but does not have to cause
  //  all items to be re rendered
  console.log('render products screen');

  //only create this function on mount so
  //  it won't re render all items
  //  passing a new handler to a sub component
  //  will cause virtual DOM compare to fail
  //  and React will re render that DOM
  const changeQty = useCallback(
    (direction) => (productId) =>
      setProducts(
        //use callback to state setter to prevent
        //  Products to be a dependency
        (products) =>
          products.map((product) =>
            product.id !== productId
              ? product
              : {
                  ...product,
                  qty: product.qty + direction,
                  //had a logical error on next line (fixed now)
                  total: product.total + direction * -1,
                }
          )
      ),
    []
  );
  //both handlers depend on changeQty but that is only created
  //  on mount
  const onAddItem = useCallback(changeQty(1), [changeQty]);
  const onRemoveItem = useCallback(changeQty(-1), [
    changeQty,
  ]);
  //FlatList renderItem has to be a function
  //  https://reactnative.dev/docs/flatlist#required-renderitem
  //  no need to memoize anything as ProductListItems is a pure
  //  component
  const renderItem = ({ item }) => (
    <ProductListItems
      product={item}
      onAddItem={onAddItem}
      onRemoveItem={onRemoveItem}
    />
  );

  return (
    <FlatList
      data={products}
      renderItem={renderItem}
      keyExtractor={(item) => item.id}
    />
  );
};

ReactDOM.render(
  <ProductsScreen />,
  document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>

 **FlatList is a built-in list on react native** 

import React from 'react';
import { SafeAreaView, View, FlatList, StyleSheet, Text, StatusBar } from 'react-native';

const DATA = [
  {
    id: '1',
    title: 'First Item',
  },
  {
    id: '2',
    title: 'Second Item',
  },
];



const App = () => {
  const renderItem = ({ item }) => (
    return(
          <Item title={item.title} />
       )
  );

  return (
    <SafeAreaView>
      <FlatList
        data={DATA}
        renderItem={renderItem}
        keyExtractor={item => item.id}
      />
    </SafeAreaView>
  );
}
Related