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 ?
Can you add useEffect hook in App.js file?.