React - setState in function gets executed X times - Updated - reactjs

The bounty expires in 2 days. Answers to this question are eligible for a +50 reputation bounty.
Cinc-B is looking for an answer from a reputable source:
Fixing the re-render situation.
Quick Overview:
I have a context with meals in it. That meals gets used in the MealDetails-Component. In this component is a list filled with a certain amount of Food-Components. If you click on the X in the Food component it gets removed otherwise you go to the FoodDetailsPage
Then when I press the button a function called deleteFood gets executed which sets the meals state new without that food. The problem is that the setState inside the function doesn't get called once but twice in this component. I tried using it in 2 other components and once it executed only once and in the other it executed 4 times.
Update
My addFood function has a pretty similiar problem. But this one gets called in another component. It adds Food to 2 different states in 2 different contexts and in both contexts the added Value is doubled. One thing I could "find" was that the console in my browser prints the a certain value I console logged inside the setState function a second time, but not via the MealsContext, but via react_devtools_backend.js.
And this only happens with those 2 functions where I have an error.
Update 2
I'm displaying the Food-Components inside the MealsDetails Component which has meals in it which comes from the MealsContext. Could this be the problem?
Update 3
I missed saying it. I have a lot more functions in this MealsContext that are changing the state. One of them is called changeFoodWeight and it gets called in the same component where addFood gets called, but it doesn't show any problems plus the react_devtools_backend.js logs nothing which is great. I added the function to the MealsContext down below.
MealsDetails.tsx
const MealDetails = () => {
const navigate = useNavigate();
let { id } = useParams();
const { meals } = useMeals();
const style = { "--percentage": "75%" } as React.CSSProperties;
return (
<div className="MealDetails">
<header className="BarcodeScannerHeader">
<ArrowBackIcon
onClick={() => {
navigate("/");
}}
sx={{ fontSize: 35 }}
></ArrowBackIcon>
<div className="HeaderText">{meals.meals[Number(id)].name}</div>
</header>
<div className="MealDetailsContent">
<span className="label">{meals.meals[Number(id)].calories} ate</span>
<hr className="SolidMealDetails"></hr>
{meals.meals[Number(id)] != null
? meals.meals[Number(id)].food.map((food: any, index: number) => <Food key={index} food={food}/>)
: "Not loading"}
</div>
<Link className="Link" to={`/AddFood/${id!}`}>
<div className="MealDetailsAddFoodButton">
<img className="SVG" id="SVG" src={SVG} alt="+" />
</div>
</Link>
</div>
);
};
MealsContext.tsx
let initialState: MealsType = {
calories: 0,
carbs: 0,
meals: [
{
calories: 0,
carbs: 0,
food: []
}
],
};
const MealsProvider = ({ children }: any) => {
const [meals, setMeals] = useState<MealsType>(initialState);
const addFood = async (
id: number,
addedFood: FoodType,
selectedLocation: string,
date: Date
) => {
//Removed the fetch for readability
console.log("func")
setMeals((prevMeals) => {
console.log("state");
const updatedMeals = prevMeals;
let foodExists = false;
updatedMeals.calories += addedFood.kcal;
updatedMeals.carbs += addedFood.carbs;
updatedMeals.meals[id].calories += addedFood.kcal;
updatedMeals.meals[id].carbs += addedFood.carbs;
updatedMeals.meals[id].food.forEach((food : FoodType) => {
if(food.code === addedFood.code){
food.kcal += addedFood.kcal;
food.carbs += addedFood.carbs;
foodExists = true;
}
})
if(!foodExists){
updatedMeals.meals[id].food.push(addedFood);
}
return {...prevMeals,updatedMeals};
});
setUser(prevUser => {
const updatedStorage = prevUser.storage;
updatedStorage.map((storage : any) => {
if(storage.location === selectedLocation){
storage.storedFood.map((storedFood : any) => {
if(storedFood.code === addedFood.code){
storedFood.weight -= addedFood.weight;
}
})
}
})
return {...prevUser, updatedStorage};
})
};
const changeFoodWeight = async (
id: number,
foodDiff: any,
selectedLocation: string,
date: Date,
newWeight: number
) => {
//Removed the fetch for readability
console.log("func");
setMeals((prevMeals) => {
console.log("state");
const updatedMeals = prevMeals;
updatedMeals.calories += foodDiff.kcalDiff;
updatedMeals.carbs += foodDiff.carbsDiff;
updatedMeals.meals[id].calories += foodDiff.kcalDiff;
updatedMeals.meals[id].carbs += foodDiff.carbsDiff;
updatedMeals.meals[id].food.forEach((food) => {
if (food.code === foodDiff.code) {
food.kcal += foodDiff.kcalDiff;
food.carbs += foodDiff.carbsDiff;
}
});
return {...prevMeals,updatedMeals};
});
setUser(prevUser => {
const updatedStorage = prevUser.storage;
updatedStorage.map((storage : any) => {
if(storage.location === selectedLocation){
storage.storedFood.map((storedFood : any) => {
if(storedFood.code === foodDiff.code){
storedFood.weight -= foodDiff.weightDiff;
}
})
}
})
return {...prevUser, updatedStorage};
})
};
const deleteFood = (id: number, deletedFood: FoodType) => {
setMeals((prevMeals) => {
const updatedMeals = prevMeals;
updatedMeals.calories -= deletedFood.kcal;
updatedMeals.carbs -= deletedFood.carbs;
updatedMeals.meals[id].calories -= deletedFood.kcal;
updatedMeals.meals[id].carbs -= deletedFood.carbs;
for(let i = 0; i < updatedMeals.meals[id].food.length; i++){
if(updatedMeals.meals[id].food[i].code === deletedFood.code){
updatedMeals.meals[id].food.splice(i, 1);
}
}
return {...prevMeals,updatedMeals};
});
};
return (
<MealsContext.Provider
value={{
meals,
deleteFood
}}
>
{children}
</MealsContext.Provider>
);
};
const useMeals = () => useContext(MealsContext);
export { MealsProvider, useMeals };
};
Food.tsx
const Food = ({ food }: any) => {
const location = useLocation();
const {deleteFood} = useMeals();
return (
<Link className="FoodContent Link" to={`/FoodDetails/`} state= {{ food: food , location: location.pathname}} >
<div className="Food">
<div className="FoodDetail">
<div className="FoodNameFoodDelete">
<div className="FoodName">{food.name}</div>
<img className="FoodDelete SVG" onClick={(e : any) => {e.preventDefault(); deleteFood(parseInt(location.pathname[location.pathname.length - 1]), food)}} src={SVG}></img>
</div>
<div className="FoodGram-FoodKcal">
<div className="FoodBrand-FoodGram">
<div className="FoodBrand">{food.brand + ","} </div>
<div className="FoodGram">
{food.weight ? food.weight + " g" : 100 + " g"}{" "}
</div>
</div>
<div className="FoodKcal">{food.kcal} kcal</div>
</div>
</div>
<div className="FoodNutritions">
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.carbs*10)/10} g</div>
<div className="FoodNutritionText">Carbs</div>
</div>
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.protein*10)/10} g</div>
<div className="FoodNutritionText">Protein</div>
</div>
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.fat*10)/10} g</div>
<div className="FoodNutritionText">Fat</div>
</div>
</div>
</div>
</Link>
);
};

The issue stems from wrapping your entire component with a Link tag which is causing some internal state-collision between your setMeals call and the Link state pass.
<Link className="FoodContent Link" to={`/FoodDetails`} state= {{ food: food , location: location.pathname}}>
<div>Block-level Elements...</div>
</Link>
Link is an inline element and is simply meant to wrap some text (or a word) that's to be used as a link somewhere. The resulting view can then access the passed-in state.
From the React Router Link v6 documentation on how to handle passing state:
The state property can be used to set a stateful value for the new location which is stored inside history state. This value can subsequently be accessed via useLocation().
<Link to="new-path" state={{ some: "value" }} />
You can access this state value while on the "new-path" route:
let { state } = useLocation();
In your situation, the Link wrap is trying to passing the food state object into the entire Food component as you are updating your entire context state (useMeals).
To better visualize the problem, I reproduced your issue "before" and "after".
In the "before" sandbox, clicking Delete Cookies removes more items than anticipated due to this issue. In the "after" sandbox the same steps work as expected. This is because only the actual link is wrapped in Link.
Essentially, you need to use your Link wrap more like this so the state it's trying to pass doesn't overwrite the state that's in the component:
const Food = ({ food }: any) => {
const location = useLocation();
const {deleteFood} = useMeals();
return (
<div className="FoodContent">
<div className="Food">
<div className="FoodDetail">
<div className="FoodNameFoodDelete">
<Link className="FoodName Link" to={`/FoodDetails/`} state= {{ food: food , location: location.pathname}} >{food.name}</Link> {/* <--- Just wrap the actual link */}
<img className="FoodDelete SVG" onClick={(e : any) => {e.preventDefault(); deleteFood(parseInt(location.pathname[location.pathname.length - 1]), food)}} src={SVG}></img>
</div>
<div className="FoodGram-FoodKcal">
<div className="FoodBrand-FoodGram">
<div className="FoodBrand">{food.brand + ","} </div>
<div className="FoodGram">
{food.weight ? food.weight + " g" : 100 + " g"}{" "}
</div>
</div>
<div className="FoodKcal">{food.kcal} kcal</div>
</div>
</div>
<div className="FoodNutritions">
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.carbs*10)/10} g</div>
<div className="FoodNutritionText">Carbs</div>
</div>
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.protein*10)/10} g</div>
<div className="FoodNutritionText">Protein</div>
</div>
<div className="FoodNutrition">
<div className="FoodNutritionGrams">{Math.round(food.fat*10)/10} g</div>
<div className="FoodNutritionText">Fat</div>
</div>
</div>
</div>
</div>
);
};
Here are the sandboxes for reference:
"Before" Sandbox
"After" Sandbox

Related

UseState value doesn't change when re-render in NextJs

I am creating a blog application on NextJs and on the page which displays posts by category i faced with a problem
i have a useState which contains all posts that i get from backend by category that i pass
The problem is when i click on the link which changes category of displayed posts i still got same posts on page, and to get actual useState value i have to reload page, how to fix it?
Here is full component
const News: FC<NewsCat> = ({ news, category }) => {
const [selectedCateg, setSelectedCateg] = useState(category);
//UseState with posts
const [postsList, setPostsList] = useState(news);
const [page, setPage] = useState(1);
const handleClick = (categ: string) => {
setSelectedCateg(categ);
};
return (
<div className={styles.wrap}>
<nav className={styles.categories}>
//Cetegory list
{list.map((i: string, index) => (
<Link href={"/category/" + listEng[index]} key={index}>
<button
className={styles.active}
onClick={() => handleClick(listEng[index])}
>
<h2>{i}</h2>
</button>
</Link>
))}
</nav>
<div className={styles.catTitle}>
<h1>{newsTranslate(selectedCateg)}</h1>
</div>
<div className={styles.mainBlock}>
{postsList.map((i: News) => (
<div key={i._id} className={styles.normal_card}>
<div className={styles.normal_card_img}>
<Link href={"/news/" + i._id}>
<img src={i?.image} alt="" />
</Link>
<div className={styles.desc}>
<div className={styles.up_desc}>
<Link href={"/category/" + category}>
<h6>{newsTranslate(i.category)}</h6>
</Link>
<h6>| {moment(i.createdAt).format("LLL")}</h6>
</div>
<Link href={"/news/" + i._id}>
<h2 className={styles.titleDesc}>
{i.title?.length > 150
? `${i.title?.substring(0, 90)}...`
: i.title}
</h2>
</Link>
</div>
</div>
<div className={styles.normal_card_desc}>
<h4>{moment(i.createdAt).format("LLL")}</h4>
</div>
</div>
))}
</div>
<div className={styles.loadMoreButton}>
<button
onClick={async () => {
setPage(page + 1);
console.log(page);
const getNextPosts = await axios.get(
"http://localhost:3000/api/category/" + category,
{
params: {
page: page,
},
}
);
setPostsList([...postsList, ...getNextPosts.data]);
}}
>
LoadMore
</button>
</div>
</div>
);
};
export default News;
export const getServerSideProps: GetServerSideProps = async ({
params,
}: any) => {
const res = await axios.get(
`http://localhost:3000/api/category/${params.category}`,
{
params: {
page: 1
}
}
);
return {
props: {
news: res?.data,
category: params?.category,
},
};
};
I know that i can fix it like this:
useEffect(() => {
setPostsList(news)
}, [news])
but in my opinion its not the best way
postsList will only change value when you call setPostsList (except for the initial value which you're passing in on first load).
So after the category is changed, you'll need to fetch the posts from the API and call setPostsList (similar to how you are doing on page change)

How i can refresh this function on started value

Hi! i have a problem with my state in React, I have two onMouse functions, the first one is to add an element and the second one is to delete, unfortunately the second one does not delete and the added element 'opacity' is rendered.
let menuItems = ['Tasks', 'Issues', 'Files', 'Raports']
const [item, setItem] = useState(menuItems)
const handleSpace = (id) => {
menuItems.splice(id, 0, 'opacity')
setItem([...item])
}
const restart = () => {
menuItems = ['Tasks', 'Issues', 'Files', 'Raports']
setItem([...item])
}
return (
<>
<div className="dashboard" style={slide.flag ? {left: '-105%'} : {left: '0%'}}>
<div className="dashboard__nav">
<ul className="dashboard__nav-list">
{item.map((item, id) => {
return <li className="dashboard__nav-item" key={id} onMouseOver={() => handleSpace(id)} onMouseLeave={restart}>{item}</li>
})}
</ul>
</div>
<div className="dashboard__array">
{tasks.map((task, id) => {
return (
<div className="dashboard__array-item" key={id}>
<div className="dashboard__array-item-header">
<p className="dashboard__array-item-header-title">{task}</p>
<button className="dashboard__array-item-header-cancel">
<FontAwesomeIcon icon={faCancel} />
</button>
</div>
<div className="dashboard__array-item-main">
<p className="dashboard__array-item-main-description">{descriptionTasks[id]}</p>
<p className="dashboard__array-item-main-button">Show More</p>
</div>
</div>
)
})}
</div>
</div>
</>
)
I already created setItem(menuItems), it removed the element 'opacity, but juz it didn't add it a second time
It seems that the two functions might be over complicating the handling of the item state.
Try handle setItem without changing another variable menuItems, so it can be used as a reset value at anytime.
Example:
const menuItems = ["Tasks", "Issues", "Files", "Raports"];
const [item, setItem] = useState(menuItems);
const handleSpace = (id) =>
setItem((prev) => {
const newItems = [...prev];
newItems.splice(id, 0, "opacity");
return newItems;
});
const restart = () => setItem(menuItems);
Hope this will help.

unable to select another option after selection with react select

Updated code: Im trying to first display carsList and only when selectedMake is selected, I would update the state with the result from filter and show another array. I tried storing carsList in updatedCarsList so it has all the cars on page load but Im missing something here.
CarOffers.jsx
const CarOffers = () => {
const [carsList, setCarsList] = useState([]);
const [updatedCarsList, setUpdatedCarsList] = useState([]);
const [selectedMake, setSelectedMake] = useState(undefined);
const getCars = () => {
axios.get(url)
.then((response) => {
return setCarsList(response.data)
})
}
const handleMakeChange = (select) => {
setSelectedMake(select.value)
}
const applyFilters = () => {
let updatedCarsList = carsList
if(selectedMake) {
updatedCarsList = carsList.filter(car => car.make === selectedMake)
setUpdatedCarsList(updatedCarsList);
} else {
setUpdatedCarsList(carsList)
}
}
useEffect(() => {
getCars()
applyFilters()
}, [ selectedMake ]);
return (
<div className="mka__wrapper-car-offers">
<div className="mka__container">
<div className="mka__content-car-offers">
<div className="mka__content-grid-offers">
<div className="item1">
< CarSlider/>
<div className="mka-responsive-item">
< DisplayCars/>
< SortingCars/>
< CarAlignment/>
</div>
</div>
<div className="item2">
<div className="mka__side-bar-divider">
< Search
carsList={carsList}/>
</div>
<div>
< FilterSideBar
carsList={carsList}
handleMakeChange={handleMakeChange} />
</div>
</div>
<div className="item3">
<Cars updatedCarsList={updatedCarsList}/>
</div>
</div>
</div>
</div>
</div>
)
}
export default CarOffers;
Cars.jsx
const Cars = ({ updatedCarsList }) => {
return (
<div className='mka__cars-grid'>
{updatedCarsList.map(car =>
<CarsItem key={car.id} car={car}/>)}
</div>
)
}
export default Cars
CarItem.jsx
const CarsItem = ({car: {year,month,transmission,mileage,price,title,link}}) => {
return (
<Fragment>
<div className="cars-item_wrapper">
<div className="cars-item_image">
<img src={link} alt="car" />
</div>
<div>
<a
className="cars-item_car-title"
href="/"
>
{title}
</a>
</div>
<div className=" cars-item_separator"></div>
<p className="cars-item_car-text">{price}</p>
</div>
</Fragment>
)
}
export default CarsItem
Move your applyFilters above getCars
Does Select need to be in <>
distinctBy... urgh.. use Set const unique = [...new Set(data.map(item => item.value))]
applyFilters... axios is async, but your setting a value so state doesn't update so no re-render? Maybe.
selectedMake - don't use null as a default, use undefined.
Hope that helps, feels like a state management issue.
... think its this ....
You are using carsList as your list of cars, however you are setting the value of carsList with setCarsList(updatedCarsList)... updatedCarsList is a filtered list of cars... only car => car.make === selectedMake so once you've selected a make your carList is only cars with the selected make.
Solution is to
Either separate the list from the filtered list
or preferably keep list, but pass the filtered state to the component that needs it... but not update state of the original list by calling setCarsList(updatedCarsList);
if (selectedMake){
updatedCarsList = updatedCarsList.filter(
car => car.make === selectedMake
)
};
setCarsList(updatedCarsList);

How can I do in React so that once a product is added to the cart if that product has already been added before it does not repeat a new object?

[ I need to avoid the duplication of an object that has already been added to the cart, so that once I add it again, I only add the quantity property of the previous object, which would be the same ]
[CartContext.js]
import React, { createContext, useState } from "react";
export const CarritoContext = createContext();
export default function CartContext({ children }) {
const [addToCarrito, setAddToCarrito] = useState([]);
[This is the function that I cannot modify so that it complies with a certain rule of not adding duplicates of an object that has already been added to the cart, only increasing its quantity of the already added object]
function addItem(item, quantity) {
setAddToCarrito(
addToCarrito.filter((elemento, pos) => {
if (elemento.item.id === item.id) {
addToCarrito[pos].quantity += quantity;
return false;
}
return true;
})
);
if (quantity === 0) {
setAddToCarrito([...addToCarrito]);
} else {
setAddToCarrito([...addToCarrito, { item, quantity }]);
}
}
function clear() {
setAddToCarrito([]);
}
function removeItem(itemId) {
const newItems = addToCarrito.filter((item) => item.item.id !== itemId);
setAddToCarrito(newItems);
}
console.log(addToCarrito);
return (
<>
<CarritoContext.Provider
value={{ addToCarrito, setAddToCarrito, clear, addItem, removeItem }}
>
{children}
</CarritoContext.Provider>
</>
);
}
[ItemAside.js]
import React, { useState, useContext } from "react";
import { Link } from "react-router-dom";
import ItemCount from "../ItemCount/ItemCount";
import { CarritoContext } from "../../context/CartContext";
const ItemAside = ({ jackets }) => {
const [quantityCarro, setQuantityCarro] = useState(0);
const [branded] = useState(jackets.brand);
let { addToCarrito } = useContext(CarritoContext);
let { setAddToCarrito } = useContext(CarritoContext);
let { clear } = useContext(CarritoContext);
let { addItem } = useContext(CarritoContext);
let { removeItem } = useContext(CarritoContext);
const onAdd = (cantidadCarro) => {
setQuantityCarro(cantidadCarro);
setAddToCarrito([
...addToCarrito,
{ item: jackets, quantity: cantidadCarro }
]);
addItem(jackets, cantidadCarro);
};
return (
<div className="container-vertical">
<aside style={{ width: "100%" }}>
<div className="container-cuadrado">
<div>
<h3>${jackets.price}</h3>
</div>
<div>
<p>and FREE Returns</p>
</div>
<div>
<p>
Delivery for <strong>$39.99</strong>
</p>
<p>
between <strong>17 - 30 April</strong>
</p>
</div>
<div>
<small>
<span className="ubicacion"></span> Deliver to Argentina
</small>
</div>
<div>
{quantityCarro ? (
<>
<Link to="/cart">
<button className="close zbutton">Buy now</button>
</Link>
<p onClick={clear}>Limpiar carro </p>
<br />
<br />
<p onClick={() => removeItem(jackets.id)}>
{`Quitar ${jackets.name} de el carro`}
</p>
</>
) : (
<ItemCount
stock={jackets.stock}
branded={branded}
initial={0}
onAdd={onAdd}
/>
)}
</div>
<div>
<div className="celwidget">
<div className="a-section a-spacing-small a-text-left celwidget">
<span className="a-declarative">
<span className="aok-align-center">
<img
alt=""
src="https://images-na.ssl-images-amazon.com/images/G/30/x-locale/checkout/truespc/secured-ssl._CB485936936_.png"
height="15px"
/>
</span>
<span className="a-letter-space"></span>
<span
className="dataspan"
style={{
cursor: "pointer",
color: "#0099C0",
}}
data-hover="We work hard to protect your security and privacy. Our payment security system encrypts your information during transmission. We don’t share your credit card details with third-party sellers, and we don’t sell your information to others."
>
Secure transaction
</span>
</span>
</div>
</div>
</div>
<div className="info-shipping">
<div>
<p>Shipping from</p>
<p>Sold by</p>
</div>
<div>
<p>Carvel</p>
<p>Carvel</p>
</div>
</div>
<div className="gift-container">
<label className="control control--checkbox">
<input type="checkbox" className="checkgift" />
<small className="small-gift">
Add a gift ticket to facilitate returns
</small>
</label>
</div>
</div>
</aside>
</div>
);
};
export default ItemAside;
I think filter is not the correct function to use. Filter needs to filter object. You are trying to mutate them as well inside. The filter function is immutable which means that it generates a new array instead of an old one.
addToCarrito.filter((elemento, pos) => {
if (elemento.item.id === item.id) {
addToCarrito[pos].quantity += quantity; // <- mutating prev value(bad)
return false;
}
return true;
})
For this task is better to use an object, rather than an array. That will simplify a lot your code.
const [products, setProducts] = useState({})
const addProduct = (item, quantity) => {
// if product already in the object - take it, otherwise add new with 0 quatity
const newProduct = { ...(product[item.id] ?? {...item, quantity: 0}) }
newProduct.quantity += 1
setProducts(products => ({
…products,
[item.id]: newProduct
})
})
}
// here the array of your products, f.e. for iteration
const productList = React.useMemo(() => Object.values(products), [products])

Update list of displayed components on deletion in React

in the beginning on my path with React I'm creating simple to-do app where user can add/remove task which are basically separate components.
I create tasks using:
addTask(taskObj){
let tasksList = this.state.tasksList;
tasksList.push(taskObj);
this.setState({tasksList : tasksList});
}
I render list of components (tasks) using following method:
showTasks(){
return (
this.state.tasksList.map((item, index) => {
return <SingleTask
taskObj={item}
removeTask = {(id) => this.removeTask(id)}
key = {index}/>;
})
);
}
method to remove specific task takes unique ID of task as an argument and based on this ID I remove it from the tasks list:
removeTask(uID){
this.setState(prevState => ({
tasksList: prevState.tasksList.filter(el => el.id != uID )
}));
}
But the problem is, when I delete any item but the last one, it seems like the actual list of components is the same only different objects are passed to those components.
For example:
Lets imagine I have 2 created componentes, if I set state.Name = 'Foo' on the first one, and state.Name='Bar' on the second one. If I click on remove button on the first one, the object associated to this component is removed, the second one becomes first but it's state.Name is now 'Foo' instead of 'Bar'.
I think I'm missing something there with correct creation/removing/displaying components in react.
Edit:
Method used to remove clicked component:
removeCurrentTask(){
this.props.removeTask(this.props.taskObj.id);
}
SingleTask component:
class SingleTask extends Component{
constructor(props) {
super(props);
this.state={
showMenu : false,
afterInit : false,
id: Math.random()*100
}
this.toggleMenu = this.toggleMenu.bind(this);
}
toggleMenu(){
this.setState({showMenu : !this.state.showMenu, afterInit : true});
}
render(){
return(
<MDBRow>
<MDBCard className="singleTaskContainer">
<MDBCardTitle>
<div class="priorityBadge">
</div>
</MDBCardTitle>
<MDBCardBody className="singleTaskBody">
<div className="singleTaskMenuContainer">
<a href="#" onClick={this.toggleMenu}>
<i className="align-middle material-icons">menu</i>
</a>
<div className={classNames('singleTaskMenuButtonsContainer animated',
{'show fadeInRight' : this.state.showMenu},
{'hideElement' : !this.state.showMenu},
{'fadeOutLeft' : !this.state.showMenu && this.state.afterInit})}>
<a
title="Remove task"
onClick={this.props.removeTask.bind(null, this.props.taskObj.id)}
className={
classNames(
'float-right btn-floating btn-smallx waves-effect waves-light listMenuBtn lightRed'
)
}
>
<i className="align-middle material-icons">remove</i>
</a>
<a title="Edit title"
className={classNames('show float-right btn-floating btn-smallx waves-effect waves-light listMenuBtn lightBlue')}
>
<i className="align-middle material-icons">edit</i>
</a>
</div>
</div>
{this.props.taskObj.description}
<br/>
{this.state.id}
</MDBCardBody>
</MDBCard>
</MDBRow>
);
}
}
Below visual representation of error, image on the left is pre-deletion and on the right is post-deletion. While card with "22" was deleted the component itself wasn't deleted, only another object was passed to it.
Just to clarify, the solution was simpler than expected.
In
const showTasks = () => taskList.map((item, index) => (
<SingleTask
taskObj={item}
removeTask ={removeTask}
key = {item.id}
/>
)
)
I was passing map index as a key, when I changed it to {item.id} everything works as expected.
In short, in the statement tasksList.push(<SingleTask taskObj={taskObj} removeTask ={this.removeTask}/>);, removeTask = {this.removeTask} should become removeTask = {() => this.removeTask(taskObj.id)}.
However, I would reconsider the way the methods addTask and showTasks are written. While the way you have written isn't wrong, it is semantically unsound. Here's what I would do:
addTask(taskObj){
let tasksList = this.state.tasksList;
tasksList.push(taskObj);
this.setState({tasksList : tasksList});
}
showTasks(){
return (
this.state.tasksList.map((item, index) => {
return <SingleTask
taskObj={item}
removeTask ={() => this.removeTask(item.id)}/>;
})
);
}
const SingleTask = (task) => {
const { taskObj } = task;
return <div onClick={task.removeTask}>
{ taskObj.title }
</div>
}
// Example class component
class App extends React.Component {
state = {
tasksList: [
{ id: 1, title: "One" },
{ id: 2, title: "Two" },
{ id: 3, title: "Three" },
{ id: 4, title: "Four" }
]
}
addTask = (taskObj) => {
let tasksList = this.state.tasksList;
tasksList.push(taskObj);
this.setState({tasksList : tasksList});
}
showTasks = () => {
return (
this.state.tasksList.map((item, index) => {
return <SingleTask
key={index}
taskObj={item}
removeTask ={() => this.removeTask(item.id)}/>;
})
);
}
removeTask(id) {
this.setState(prevState => ({
tasksList: prevState.tasksList.filter(el => el.id != id )
}));
}
render() {
return (
<div className="App">
<div> {this.showTasks()} </div>
</div>
);
}
}
// Render it
ReactDOM.render(
<App />,
document.body
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Resources