First value in array is not getting updated - javascript

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.

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++) {
...
}

React UseContext change not re-rendering component

I am trying to make a simple 'Nonogram'/'Picross' game using React to learn UseContext and UseReducer, but am puzzled as to why my top component (App) is not re-rendering when a value it uses changes. Perhaps I am missing something basic, but I've read through documentation and examples online and can't see why it is not re-rendering.
Expectation: User goes on the application, clicks on the squares to change their value (draw a cross by clicking on the squares), and the text underneath the board reads "Congratulations!", as it is based on the value of 'isComplete'
Problem: As above, but 'Keep trying' remains.
I added a button to see the boardState as defined in the UseReducer function, too.
Code is as follows:
App.js
import './App.css';
import { useReducer } from 'react';
import Table from './Table';
import BoardContext from './BoardContext';
import boardReducer from './BoardReducer';
function App() {
//Puzzle layout
const puzzleArray = [
[true, false, true],
[false, true, false],
[true, false, true]
];
//Creating a set of blank arrays to start the game as the userSelection
const generateUserSelection = () => {
const userSelection = [];
puzzleArray.forEach(row => {
let blankRow = [];
row.forEach(square => {
blankRow.push(false)
});
userSelection.push(blankRow);
})
return userSelection;
};
//Initial Context value
const boardInfo = {
puzzleName: "My Puzzle",
puzzleArray: puzzleArray,
userSelection: generateUserSelection(),
isComplete: false
};
const [ boardState, dispatch ] = useReducer(boardReducer, boardInfo)
return (
<BoardContext.Provider value={{board: boardState, dispatch}}>
<div className="App">
<header className="App-header">
<p>
Picross
</p>
<Table />
</header>
<div>
{boardState.isComplete ?
<div>Congratulations!</div>
: <div>Keep trying</div>
}
</div>
<button onClick={() => console.log(boardState)}>boardState</button>
</div>
</BoardContext.Provider>
);
}
export default App;
Table.jsx:
import { useContext, useEffect } from 'react';
import './App.css';
import Square from './Square';
import BoardContext from './BoardContext';
function Table() {
useEffect(() => {console.log('table useEffect')})
const { board } = useContext(BoardContext);
const generateTable = solution => {
const squareLayout = []
for (let i = 0; i < solution.length; i++) {
const squares = []
for (let j = 0; j < solution[i].length; j++) {
squares.push(
<Square
position={{row: i, column: j}}
/>
);
};
squareLayout.push(
<div className="table-row">
{squares}
</div>
);
};
return squareLayout;
};
return (
<div className="grid-container">
{generateTable(board.puzzleArray)}
</div>
);
}
export default Table;
Square.jsx
import { useContext, useState } from 'react';
import './App.css';
import BoardContext from './BoardContext';
function Square(props) {
const { board, dispatch } = useContext(BoardContext)
const [ isSelected, setIsSelected ] = useState(false);
const { position } = props;
const handleToggle = () => {
console.log(board)
board.userSelection[position.row][position.column] = !board.userSelection[position.row][position.column]
dispatch(board);
setIsSelected(!isSelected);
}
return (
<div className={`square ${isSelected ? " selected" : ""}`}
onClick={handleToggle}
>
{position.row}, {position.column}
</div>
);
}
export default Square;
Thanks
Edit: I know for a simple application like this it would be very easy to pass down state through props, but the idea is to practice other hooks, so wanting to avoid it. The ideas I am practicing in this would ideally be extensible to bigger projects in the future.
Edit 2: As requested, here's my BoardReducer.js file:
const boardReducer = (state, updateInfo) => {
let isComplete = false;
if (JSON.stringify(updateInfo.userSelection) === JSON.stringify(state.puzzleArray)) {
isComplete = true;
}
updateInfo.isComplete = isComplete;
return updateInfo;
}
export default boardReducer;
(using JSON.stringify as a cheap way to check matching arrays as it's only a small one for now!)
Issue
You are mutating your state object in a couple places:
const handleToggle = () => {
console.log(board);
board.userSelection[position.row][position.column] = !board.userSelection[position.row][position.column]; // <-- mutation!
dispatch(board);
setIsSelected(!isSelected);
}
And in reducer
const boardReducer = (state, updateInfo) => {
let isComplete = false;
if (JSON.stringify(updateInfo.userSelection) === JSON.stringify(state.puzzleArray)) {
isComplete = true;
}
updateInfo.isComplete = isComplete; // <-- mutation!
return updateInfo; // <-- returning mutated state object
}
Since no new state object is created React doesn't see a state change and doesn't rerender your UI.
Solution
useReducer will typically employ a "redux" pattern where the reducer function consumes the current state and an action to operate on that state, and returns a new state object.
You should dispatch an action that toggles the user selection and checks for a complete board.
Board Reducer
When updating state you should shallow copy any state objects that you are updating into new object references, starting with the entire state object.
const boardReducer = (state, action) => {
if (action.type === "TOGGLE") {
const { position } = action;
const nextState = {
...state,
userSelection: state.userSelection.map((rowEl, row) =>
row === position.row
? rowEl.map((colEl, col) =>
col === position.column ? !colEl : colEl
)
: rowEl
)
};
nextState.isComplete =
JSON.stringify(nextState.userSelection) ===
JSON.stringify(state.puzzleArray);
return nextState;
}
return state;
};
Create an action creator, which is really just a function that returns an action object.
const togglePosition = position => ({
type: "TOGGLE",
position
});
Then the handleToggle should consume/pass the row and column position in a dispatched action.
const handleToggle = () => dispatch(togglePosition(position));
Simple Demo
Demo Code:
const puzzleArray = [
[true, false, true],
[false, true, false],
[true, false, true]
];
const userSelection = Array(3).fill(Array(3).fill(false));
const togglePosition = (row, column) => ({
type: "TOGGLE",
position: { row, column }
});
const boardReducer = (state, action) => {
if (action.type === "TOGGLE") {
const { position } = action;
const nextState = {
...state,
userSelection: state.userSelection.map((rowEl, row) =>
row === position.row
? rowEl.map((colEl, col) =>
col === position.column ? !colEl : colEl
)
: rowEl
)
};
nextState.isComplete =
JSON.stringify(nextState.userSelection) ===
JSON.stringify(state.puzzleArray);
return nextState;
}
return state;
};
export default function App() {
const [boardState, dispatch] = React.useReducer(boardReducer, {
puzzleArray,
userSelection,
isComplete: false
});
const handleClick = (row, column) => () =>
dispatch(togglePosition(row, column));
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>{boardState.isComplete ? "Congratulations!" : "Keep Trying"}</div>
<div>
{boardState.userSelection.map((row, r) => (
<div key={r}>
{row.map((col, c) => (
<span
key={c}
className={classnames("square", { active: col })}
onClick={handleClick(r, c)}
/>
))}
</div>
))}
</div>
</div>
);
}

update value of an object in an array - react hooks

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;
})
);
};

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);};

In a React,Redux app I want to calculate a subtotal (price * qtyCount) for each object in an array of objects using a selector

I edited my question and added the component in question.
I suspect maybe just me not understanding the syntax.
My current output in component looks like this 25.0018.00 it should be 25.00 for 1 object and 18.00 for next.
I have an array of objects that look like this:
Json:
counters: [
{
id: "lam1g8uzn",
windowType: "First ",
windowLocation: "Out",
price: 5,
qtyCount: 5,
imgUrl:'long string'
},
{
id: "r5hamynf3",
windowType: "Second ",
windowLocation: "In",
price: 2,
qtyCount: 9,
imgUrl: 'long string'
}
]
Here is my selector
const selectCounter = (state) => state.counter;
export const selectCounterItems = createSelector(
[selectCounter],
(counter) => counter.counters,
);
This is the selector in question
export const selectCounterSubtotal = createSelector(
[selectCounterItems],
(counters) => counters.map((counter) => (
(counter.qtyCount * counter.price).toFixed(2)
)),
);
Here is my component where subtotal is displayed
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import { selectCounterSubtotal } from '../../redux/counter/counter-
selectors';
const SubTotalDisplay = ({ subTotal }) => (
// const subtotal = (qty * price).toFixed(2);
<div className="subtotal-diaplay">
<span> Subtotal $</span>
<span className="subtotal">{subTotal}</span>
</div>
);
const mapStateToProps = createStructuredSelector({
subTotal: selectCounterSubtotal,
});
export default connect(mapStateToProps, null)(SubTotalDisplay);
You're passing an array of strings to SubtotalDisplay. When React gets an array node it renders each of the items in the array. That's why you're getting "25.0018.00".
Consider updating the SubtotalDisplay component to render all subtotals passed to it:
const SubTotalDisplay = ({ subTotals }) => (
subTotals.map((subTotal, index) => (
<div className="subtotal-display" key={index}>
<span> Subtotal $</span>
<span className="subtotal">{subTotal}</span>
</div>
))
);
const mapStateToProps = createStructuredSelector({
subTotals: selectCounterSubtotal,
});
I found that my problem was actually in another utility function where I was mistakenly mutating data.
//Right Way
export const resetAllCounters = (counters) => counters.map((counter) => ({
...counter, qtyCount: 0 }));
// Wrong way mutating data
export const resetAllCounters = (counters) => counters.map((counter) =>
Object.assign(counter, { qtyCount: 0 }));

Categories

Resources