update value of an object in an array - react hooks - javascript

I want to increment the counter value of an item on click, I've tried to find the solution in the docs and I watched tutorials but I can't find the solution.
FruitCounter.js
import { Fragment, useState } from "react"
import Fruit from "./Fruit"
const data = [
{ id: 1, name: "🍋", counter: 0 },
{ id: 2, name: "🍒", counter: 0 },
]
const FruitCounter = () => {
const [fruits, setFruits] = useState(data)
const clickHandler = (fruit) => {
// Increment 'counter value of clicked item by 1'
}
return (
<Fragment>
{fruits.map((fruit) => {
return (
<Fruit
key={fruit.id}
{...fruit}
clickHandler={() => clickHandler(fruit)}
/>
)
})}
</Fragment>
)
}
export default FruitCounter
Fruit.js
import React from "react"
const Fruit = ({ counter, name, clickHandler }) => {
return (
<button type="button" className="fruit" onClick={clickHandler}>
<p>{counter}</p>
<h2>{name}</h2>
</button>
)
}
export default Fruit

You can try this
const clickHandler = (fruit) => {
setFruits(
fruits.map((x) => {
if (x.id === fruit.id)
return {
...x,
counter: x.counter + 1,
};
return x;
})
);
};

Related

Why do I get NaN value in react?

Whilst I am doing cart in react, I have no idea why I keep getting NaN value - only from a specific object data.
When I have the following data list:
#1 ItemsList.js
export const ItemsList = [
{
id: 1,
name: "VA-11 Hall-A: Cyberpunk Bartender Action",
price: 110000,
image: cover1,
link: "https://store.steampowered.com/app/447530/VA11_HallA_Cyberpunk_Bartender_Action/?l=koreana",
},
...
{
id: 6,
name: "Limbus Company",
price: 110000,
image: cover6,
link: "https://limbuscompany.com/",
},
];
And the following code, please look at the comment line.
#2 Goods.jsx
import React, { useContext } from "react";
import "./Goods.css";
import { DataContext } from "../../components/context/DataContext";
export const Goods = (props) => {
const { id, name, price, image, link } = props.shopItemProps;
const { cartItems, addItemToCart, removeItemFromCart } =
useContext(DataContext);
const cartItemStored = cartItems[id];
return (
<div className="goods">
<div className="goods-id">{id}</div>
<img src={image} alt="thumbnail_image" className="goods-image" />
<div className="goods-name">{name}</div>
<div className="goods-price">${price}</div>
<button>
<a href={link} className="goods-link">
Official Store Page
</a>
</button>
<div className="cart-button">
<button onClick={() => removeItemFromCart(id)}>-</button>
// ★Maybe here? but why do I get NaN only for id:6? Others work well.
{cartItemStored > -1 && <> ({cartItemStored}) </>}
<button onClick={() => addItemToCart(id)}>+</button>
</div>
</div>
);
};
What should I do to solve NaN? There seems to be no way to make that value as int in this case. Or do you see any problem from the above code block?
Edited
Sorry for confusing you. Here are the additional code related.
#3. DataContext.js (where cartItems state exists)
import React, { createContext, useState } from "react";
import { ItemsList } from "../ItemsList";
export const DataContext = createContext(null);
const getDefaultCart = () => {
let cart = {};
for (let i = 1; i < ItemsList.length; i++) {
cart[i] = 0;
}
return cart;
};
export const DataContextProvider = (props) => {
const [cartItems, setCartItems] = useState(getDefaultCart);
const checkoutTotalSum = () => {
let totalAmount = 0;
for (const item in cartItems) {
if (cartItems[item] > 0) {
let itemInfo = ItemsList.find((product) => product.id === Number(item));
totalAmount += cartItems[item] * itemInfo.price;
}
}
return totalAmount;
};
const addItemToCart = (itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] + 1 }));
};
const removeItemFromCart = (itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] - 1 }));
};
const updateCartItemCount = (newAmount, itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: newAmount }));
};
const contextValue = {
cartItems,
addItemToCart,
removeItemFromCart,
updateCartItemCount,
checkoutTotalSum,
};
// console.log(cartItems);
return (
<DataContext.Provider value={contextValue}>
{props.children}
</DataContext.Provider>
);
};
The issue is in the function you are using to set the initial value of cartItems, more specifically, in the for loop. This line is the culprit: i < ItemsList.length, when in your case, it should be i <= ItemsList.length. Why? because you are not including the last element of ItemsList on the cart object (you are initializing the i counter with 1 and ItemsList's length is 6).
So, when you call addItemToCart:
const addItemToCart = (itemId) => {
setCartItems((prev) => ({ ...prev, [itemId]: prev[itemId] + 1 }));
};
And try to update the value corresponding to the last element of ItemsList which is 6 in cartItems, you're getting: '6': undefined + 1 because again, you did skip the last element in the for loop. This results in NaN.
You also have the option of initializing i with 0 and preserve this line: i < ItemsList.length, or:
for (let i = 1; i < ItemsList.length + 1; i++) {
...
}

First value in array is not getting updated

I'm building a shopping cart where the user can change the amount they want to invest per asset, and then see how much their investment is worth when compounded 10% over 5 years.
I first have my Checkout component:
import React, { useEffect, useState } from "react";
import CartInner from "./CartInner";
export default function Checkout() {
const [cart, setCart] = useState<any>([
{ price: 2000, id: 1 },
{ price: 4000, id: 2 }
]);
const [projectedGrowth, setProjectedGrowth] = useState<any>([]);
const handleOnChange = (index: any, e: any) => {
const newValue = {
price: Number(e.target.value),
id: index + 1,
property_id: index + 1
};
const newArray = [...cart];
newArray.splice(index, 1, newValue);
setCart(newArray);
};
const getGrowthAmount = (index: any) => {
console.log(cart[index].price, "default values for price");
const newValue = calcTotalCompoundInterest(cart[index].price, 5, 10);
const newArray = [...projectedGrowth];
newArray.splice(index, 1, newValue);
setProjectedGrowth(newArray);
};
// whenever the cart is updated call the getGrowthAmount function
useEffect(() => {
for (let index = 0; index < cart.length; index++) {
getGrowthAmount(index);
}
}, [cart]);
console.log(projectedGrowth, "projectedGrowth");
console.log(cart, "cart");
return (
<>
<form className="grid gap-8 grid-cols-3">
<div className="col-span-2">
{cart.map((element, index) => {
return (
<fieldset key={index}>
<CartInner
// projectedGrowth={projectedGrowth[index]}
element={element}
handleOnChange={(e) => handleOnChange(index, e)}
defaultValues={cart[index]?.price}
/>
</fieldset>
);
})}
</div>
<button>Proceed to payment</button>
</form>
</>
);
}
Which allows the user to change their investment amount and calculate compound interest:
function calcTotalCompoundInterest(total: any, year: any, rate: any) {
var interest = rate / 100 + 1;
return parseFloat((total * Math.pow(interest, year)).toFixed(4));
}
My problem is only the second value is getting updated, not the first. E.g if the first input I write 100 and the second 100,000 then the projectedGrowth array will return:
(2) [6442.04, 16105.1]
Which is correct for the second amount (but not for the first).
Here's the Codesandbox
Here's the child component:
import PureInput from "./PureInput";
import React, { useEffect, useState, useMemo } from "react";
const CartInner = React.forwardRef(
(
{
handleOnChange,
price,
projectedGrowth,
defaultValues,
id,
...inputProps
}: any,
ref: any
) => {
return (
<input
min={200}
max={price}
handleOnChange={handleOnChange}
type="number"
step={200}
defaultValue={defaultValues}
id={id}
ref={ref}
{...inputProps}
/>
);
}
);
export default CartInner;
What am I doing wrong here?
How can I get the array to return the correct compounded values (both Onload and Onchange when the user enters into the input)?
Issue is with the splice. accessing the index directly from the array works
const getGrowthAmount = (index: any) => {
console.log(cart[index].price, "default values for price");
const newValue = calcTotalCompoundInterest(cart[index].price, 5, 10);
console.log(newValue, "newValue");
projectedGrowth[index] = newValue;
console.log(projectedGrowth, "list");
setProjectedGrowth(projectedGrowth);
};
Demo
The correct code is this one:
import React, { useEffect, useState, useMemo } from "react";
import { useForm, Controller } from "react-hook-form";
import CartInner from "./CartInner";
function calcTotalCompoundInterest(total: any, year: any, rate: any) {
var interest = rate / 100 + 1;
return parseFloat((total * Math.pow(interest, year)).toFixed(4));
}
export default function Checkout() {
const [cart, setCart] = useState<any>([
{ price: 2000, id: 1 },
{ price: 4000, id: 2 }
]);
const [projectedGrowth, setProjectedGrowth] = useState<any>([]);
const onSubmit = async (data: any) => {};
const handleOnChange = (index: any, e: any) => {
console.log(index);
const newValue = {
price: Number(e.target.value),
id: index + 1,
property_id: index + 1
};
const newArray = [...cart];
newArray[index] = newValue;
setCart(newArray);
};
const getGrowthAmount = () => {
const newArray = [...projectedGrowth];
for (let index = 0; index < cart.length; index++) {
console.log(index);
//console.log(cart[index].price, "default values for price");
console.log(cart);
const newValue = calcTotalCompoundInterest(cart[index].price, 5, 10);
newArray[index] = newValue;
//newArray.splice(index, 1, newValue);
}
setProjectedGrowth(newArray);
};
// whenever the cart is updated call the getGrowthAmount function
useEffect(() => {
//for (let index = 0; index < cart.length; index++) {
getGrowthAmount();
//}
}, [cart]);
console.log(projectedGrowth, "projectedGrowth");
console.log(cart, "cart");
return (
<>
<form className="grid gap-8 grid-cols-3">
<div className="col-span-2">
{cart.map((element, index) => {
return (
<fieldset key={index}>
<CartInner
// projectedGrowth={projectedGrowth[index]}
element={element}
handleOnChange={(e) => handleOnChange(index, e)}
defaultValues={cart[index]?.price}
/>
</fieldset>
);
})}
</div>
<div className="bg-slate-200 p-8 flex flex-col items-stretch">
<button>Proceed to payment</button>
</div>
</form>
</>
);
}
the error is on the useEffect function. In your implementation the function on all change call iteratively on the index the getGrowthAmount function that create a new array each time.
You should call only one time the getGrowthAmount function and in the cycle value the array at the index of the cycle. Then you can update pass the array to be updated.

Avoid render the button after onClick

I'm trying to render some tags from a source and then put it into a buttons in random form but the issue comes after click the button my component re-render for no reason.
why can i do?...
const Blog = () => {
const [articles, setArticles] = useState([]);
const [tags, setTags] = useState([]);
// consult to database
const getData = async () => {
try {
// get aticles, tags
setArticles([
{ name: "title1", id_tag: 1 },
{ name: "title2", id_tag: 2 }
]);
setTags([
{ id: 1, name: "bell" },
{ id: 2, name: "fashion" }
]);
} catch (e) {
console.log(e);
}
};
useEffect(() => {
getData();
}, []);
const getRandomTag = (i) => {
const newTags = tags;
const tag = newTags[Math.floor(Math.random() * newTags.length)];
return (
<button key={i} onClick={() => filterByTag(tag.id)}>
{tag.name}
</button>
);
};
// filter the article by tag
const filterByTag = (id) => {
setArticles(articles.filter((article) => article.id_tag === id));
};
return (
<>
<ul>
{articles.map((article, i) => (
<li key={i}>{article.name}</li>
))}
</ul>
<h4>TAGS</h4>
<ul>{tags.map((tag, i) => getRandomTag(i))}</ul>
</>
);
export default Blog;
https://codesandbox.io/s/heuristic-solomon-sp640j?from-embed=&file=/src/Blog.js
That's because your component re-renders whenever any state inside it or any props it receives changes either by value or reference. And whenever you map something without caching its value it maps on each rerender. You have tu either split it into other component or use caching.
import { useCallback, useEffect, useMemo, useState } from "react";
const Blog = () => {
const [articles, setArticles] = useState([]);
const [tags, setTags] = useState([]);
// consult to database
const getData = async () => {
try {
// get aticles, tags
setArticles([
{ name: "title1", id_tag: 1 },
{ name: "title2", id_tag: 2 },
{ name: "title3", id_tag: 3 },
{ name: "title4", id_tag: 4 },
{ name: "title5", id_tag: 5 },
{ name: "title6", id_tag: 6 }
]);
setTags([
{ id: 1, name: "bell" },
{ id: 2, name: "fashion" },
{ id: 3, name: "fancy" },
{ id: 4, name: "tag" },
{ id: 6, name: "name" },
]);
} catch (e) {
console.log(e);
}
};
useEffect(() => {
getData();
}, []);
const filterByTag = useCallback((id) => {
setArticles(currentArticles => currentArticles.filter((article) => article.id_tag === id));
}, [setArticles]);
const getRandomTag = useCallback(
(i) => {
const newTags = tags;
const tag = newTags[Math.floor(Math.random() * newTags.length)];
return (
<button key={i} onClick={() => filterByTag(tag.id)}>
{tag.name}
</button>
);
},
[tags, filterByTag]
);
// filter the article by tag
const renderTags = useMemo(() => {
return tags.map((tag, i) => getRandomTag(i));
}, [tags, getRandomTag]);
return (
<>
<ul>
{articles.map((article, i) => (
<li key={i}>{article.name}</li>
))}
</ul>
<h4>TAGS</h4>
<ul>{renderTags}</ul>
</>
);
};
export default Blog;
Here you have a working example of caching https://codesandbox.io/s/sad-shadow-8pogoc?file=/src/Blog.js

Child component doesn't update correctly with state update in parent component

Simply i have a list item, that contain a list of names, clicking any list item change the color of that item, this is my logic:
const App = () => {
const items = [
{
name: 'peter',
id: 1
},
{
name: 'Mark',
id: 2
},
{
name: 'john',
id: 3
}
]
const [id, setId] = useState(null);
const [names, setNames] = useState(items)
const setClickedItemId = (id) => {
setId(id)
}
const turnItemRed = () => {
setNames(prev => prev.map(i => i.id === id ? {...i, color: 'red' } : i))
}
return (
<div className="app">
<ul className="items">
{
names.map(i => {
return (
<Item
item={i}
setClickedItemId={setClickedItemId}
turnItemRed={turnItemRed}
/>
)
})
}
</ul>
</div>
)
}
function Item({item, ...props}) {
const { name, id} = item;
const { setClickedItemId, turnItemRed } = props;
return (
<li
className={`${item.color === 'red' ? 'red' : ''}`}
onClick={() => {
setClickedItemId(id);
turnItemRed()
}}
>{name}</li>
)
}
ReactDOM.render(<App />, document.getElementById('root'))
This renders a list of items, i need two clicks to have an item turning into red, which means the child component doesn't catch the most recent version of state, but:
Just adding that line of code before the return statement in parent components,
const showingItems = names.map(i => i.id === id ? {...i, color: 'red'} : i)
and then using that variable showingItems to render the list instead of state variable names make it right and don't know why
So, why the child components Items doesn't get the most recent version of the state while storing the state in a variable makes it work??
State updates are batched and your onClick triggers 2 functions which does state updates. The second function doesn't receive updated value due to the async behaviour.
Just pass the id to turnItemRed function instead of grabbing it from state.
App
const turnItemRed = (id) => { //<----take the id
setNames(prev => prev.map(i => i.id === id ? {...i, color: 'red' } : i))
}
Item
function Item({item, ...props}) {
const { name, id} = item;
const { setClickedItemId, turnItemRed } = props;
return (
<li
className={`${item.color === 'red' ? 'red' : ''}`}
onClick={() => {
setClickedItemId(id);
turnItemRed(id) //<---- pass the id
}}
>{name}</li>
)
}
Edit
A quick demo of the above issue and the fix is here in the demo. . Just adding this so it might help other readers in future.
import React,{useState} from 'react';
export default () => {
const items = [
{
name: 'peter',
id: 1
},
{
name: 'Mark',
id: 2
},
{
name: 'john',
id: 3
}
]
const [id, setId] = useState(null);
const [names, setNames] = useState(items)
const setClickedItemId = (id) => {
setId(id);
turnItemRed(id);
}
const turnItemRed = (id) => {
setNames(prev => prev.map(i => i.id === id ? { ...i, color: 'red' } : i))
}
return (
<div className="app">
<ul className="items">
{
names.map(i => {
return (
<Item
item={i}
setClickedItemId={setClickedItemId}
turnItemRed={turnItemRed}
/>
)
})
}
</ul>
</div>
)
}
function Item({ item, ...props }) {
const { name, id } = item;
const { setClickedItemId, turnItemRed } = props;
return (
<li
style={{ color: item.color === 'red' ? 'red' : ''}}
onClick={() => {
setClickedItemId(id);
}}
>{name}</li>
)
}

In what condition it will re-render while using react custom hooks

I tried a sample in using react hook, make it a custom hook.
The problem is the simple hook useCount() goes fine, but the hook useCarHighlight() intending to switch highlight line would not cause re-render.
I see it is the same of the two, is anything wrong I should attention for about this?
I made a sandbox here: https://codesandbox.io/s/typescript-j2xtf
Some code below:
// index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import useCarHighlight, { Car } from "./useCarHighlight";
import useCount from "./useCount";
const myCars: Car[] = [
{ model: "C300", brand: "benz", price: 29000, ac: "auto ac" },
{ model: "Qin", brand: "byd", price: 9000 }
];
const App = () => {
const { cars, setHighlight } = useCarHighlight(myCars, "Qin");
const { count, increase, decrease } = useCount(10);
console.log(
`re-render at ${new Date().toLocaleTimeString()},
Current highlight: ${
cars.find(c => c.highlight)?.model
}`
);
return (
<div>
<ul>
{cars.map(car => {
const { model, highlight, brand, price, ac = "no ac" } = car;
return (
<li
key={model}
style={{ color: highlight ? "red" : "grey" }}
>{`[${brand}] ${model}: $ ${price}, ${ac}`}</li>
);
})}
</ul>
<button onClick={() => setHighlight("C300")}>highlight C300</button>
<button onClick={() => setHighlight("Qin")}>highlight Qin</button>
<hr />
<h1>{`Count: ${count}`}</h1>
<button onClick={() => increase()}>+</button>
<button onClick={() => decrease()}>-</button>
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
// useCarHighlight.ts
import { useState } from "react";
export type Car = {
model: string;
brand: string;
price: number;
ac?: "auto ac" | "manual ac";
};
export default function(
initialCars: Car[],
initialSelectedModel: string
): {
cars: Array<Car & { highlight: boolean }>;
setHighlight: (selMod: string) => void;
} {
const carsHighlight = initialCars.map(car => ({
...car,
highlight: initialSelectedModel === car.model
}));
const [cars, setCars] = useState(carsHighlight);
const setHighlight = (selMod: string) => {
cars.forEach(car => {
car.highlight = car.model === selMod;
});
setCars(cars);
};
return {
cars,
setHighlight
};
}
// useCount.ts
import { useState } from "react";
export default function useCount(initialCount: number) {
const [state, setState] = useState(initialCount);
const increase = () => setState(state + 1);
const decrease = () => setState(state - 1);
return {
count: state,
increase,
decrease
};
}
Unlike class components, mutating state of hooks does not queue a re-render, when using hooks you have to update your state in an immutable way.
Also, when calculating the next state based on the previous state it is recommended to use a functional update and read the previous state from the first argument of the function.
const setHighlight = (selMod: string) => {
setCars(prevState =>
prevState.map(car => ({
...car,
highlight: car.model === selMod
}))
);
};
Here is a good resource about immutable update patterns
Dont use forEach in setHighlight, use map instead
const setHighlight = (selMod: string) => {
const newCars = cars.map(car => ({
...car,
highlight: car.model === selMod
}));
setCars(newCars);
};
Use map instead of forEach as the address of car object isn't getting changed when you update highlight property in car.
const setHighlight = (selMod: string) => {
let carsTemp = cars.map(car => ({
...car,
highlight : car.model === selMod
}));
setCars(carsTemp);};

Categories

Resources