Problems with multiple rerendering in React - Add to Favourite List example

165 Views Asked by At

I am building a small framework for e-commerce or blog app using React.

One of the features of this framework is an "Add to Favourite List" function.

The app tree works as follow :

App file stores the selectedItem chosen by the Menu, and then App dispatches this selectedItem to a displayCategoryItems, which is the central view of the app.

The data is managed by the state provided by the useHistory hook, so I'm not using a classical props data management.

Once data ID is passed on to displayCategoryItems, it's being fetched on Firestore using an async function.

Data retrieved is passed to a List component which loops through the Firestore object and makes the data readable, and creates as much Item as necessary in the display area using forEach.

Each Item has a function inherited from App which allows to add this Item to the favs (favourite) array state in App.

Problem :

Each time an Item is being added to the favs state, it causes rerenders of displayCategoryItems and List, and also to all the Item in it, which can be problematic since the list of Item can be potentially huge.

I thought about wrapping the handleFavs function in a useCallback, but if I do so, the function loses its ability to check whether the Item name is already in the favs, which is not good, since it allows to add the item multiple times, and it does not prevent the multiple rerenders of Item anyway

Here's the code :

App


import React, { useCallback, useEffect, useState} from "react";
import Menu from "./menu";
import "./main.css";
import MenuData from "./menuData";
import { BrowserRouter as Router, Switch, Route} from 'react-router-dom';
import Footer from "./footer";
import Header from "./header";
import DisplayCategoryItems from "./displayCategoryItems";

function App() {

  const [selectedItem, setMenuItem] = useState(null)
  const [favs, setFavs] = useState([])

  const handleMenuItem = useCallback((menuItem) => {
    setMenuItem(menuItem)
  }, [])

  const handleFavs = (newFav) => {
    if (favs.indexOf(newFav) > -1) {
      return;
    } else {
      setFavs(previousFavs => [...previousFavs, newFav])
    }
  }

  return (
    <div className="App">
      <Router>
        <Header
          handleMenuItem={handleMenuItem} 
        ></Header>
        <Menu 
          // data for menu comes in an object stored locally : MenuData
          items={MenuData}
          handleMenuItem={handleMenuItem} 
          // for giving CSS style to the selected menuItem
          selectedItem={selectedItem}
        ></Menu>
        <div id="wrapper">
          <Switch>
            <Route path = "/menu/:categoryItem/" exact render = {() => 
                <DisplayCategoryItems
                  handleFavs={handleFavs}
                ></DisplayCategoryItems>}>
            </Route>
          </Switch>
        </div>
      </Router>
      <Footer></Footer>
    </div>
  );
}

export default App;

displayCategoryItems child of App

import React, { useEffect, useState} from "react";
import { collection, getDocs, where, query } from 'firebase/firestore'
import { db } from "./firebase-config";
import { useHistory} from "react-router-dom";
import Liste from "./liste";

const DisplayCategoryItems = (props) => {

    const [categoryItemData, setCategoryData] = useState([])

    const history = useHistory()

    const getCategoryData = async () => {
        const queryCategories = collection(db, "items")
        let data = query(queryCategories, where("category", "==", history.location.state.entry))
        const querySnapshot = await getDocs(data);
        setCategoryData(querySnapshot)
    }

    useEffect(() => {
        getCategoryData()
    }, [])

    return (
        <>
            <div 
                id="displayCategoryItems" 
                className="display"
            >
                <Liste
                    data={categoryItemData}
                    handleFavs={props.handleFavs}
                ></Liste>
            </div>
            {console.log("Display Category Items")}
        </>
    )
};

export default DisplayCategoryItems;

List child of DisplayCategoryItems

import React from "react";
import Item from "./item";

const Liste = (props) => {

    const categoryItemDataList=[]
    props.data.forEach(doc => {
       // items can have several different packaging, hence this For loop
        for (let i = 0; i < doc.data().packaging.length; i++) {
            categoryItemDataList.push(
                <Item 
                    key={doc.data().name + i}
                    data={doc.data()}
                    handleFavs={props.handleFavs}
                ></Item >
            )
        }
    })

    return (
        <>  <div>
                {categoryItemDataList}
            </div>
            {console.log("Liste")}
        </>
    )
};

export default Liste;

Item component for List

import React from "react";

const Item = (props) => {

    return (
        <>
            <div 
                className="item"
            >
                <div
                    className="itemBody"
                >{props.data.name}</div>
                <table className="ItemFooter">
                    <tbody>
                        <tr>
///////// favs are added here ////////////////
                            <th className="ItemddToFav" onClick={() => props.handleFavs(props.data.name)}></th>
                            <th className="ItemBuy">Buy</th>
                            <th className="ItemRight"></th>
                        </tr>
                    </tbody>
                </table>
                </div>
            {console.log("Item")}
        </>
    )
}

export default Item;

And the other tree of the project, the Menu

Menu child of App

import React from "react";
import MenuItem from "./menuItem";

const Menu = (props) => {

    const menuItemList = []

    for (const [entry, value] of Object.entries(props.items)) {
        menuItemList.push(
            <MenuItem 
                key={entry}
                entry={entry}
                handleMenuItem={props.handleMenuItem} 
                value={value.value} 
                classN={props.selectedItem === entry ? "menuItem selected" : "menuItem"}
            ></MenuItem>
        )
    }

    return (
        <>  <div id="menuWrapper">
                <table id="menu">
                    <tbody>
                        <tr>
                            {menuItemList}
                        </tr>
                    </tbody>
                </table>
            </div>
            {console.log("menu")}
        </>
    )
};

export default React.memo(Menu);

MenuItem component for Menu

import React from "react";
import { useHistory} from "react-router-dom";

const MenuItem = (props) => {

    const history = useHistory()

    const handleClick = () => {
        props.handleMenuItem(props.entry)
        history.push(`/menu/${props.entry}`)
        history.replace({
            state : props.entry
        })
    }

    return (
        <>
            <th
                onClick={() => handleClick()} 
                className={props.classN}
            >{props.value}</th>
            {console.log("MenuItem's render")}
        </>
    )
};

export default React.memo(MenuItem);

Anyone has an idea on how to stop these multiple rerendering while managing the favs state properly ?

1

There are 1 best solutions below

0
On

Can you add useEffect hook in App.js file?.

useEffect(()=>{},[favs])