Hello guys I have an array like this :
[
{
"name": "test",
"amount": 794.651786,
"id": "60477897fd230655b337a1e6"
},
{
"name": "test2",
"amount": 10.80918,
"id": "60477bfbfd230655b337a1e9"
}
]
And i wan't to make the total of every amount.
I tried by using the useState hook like this :
const [total, setTotal] = useState(Number);
array.map((item) => {
setTotal(total + item.amount);
});
but it doesn't seems to work as expected.
You could use the reduce method, see docs.
setTotal(array.reduce((sum, item) => sum + item.amount, 0))
I invite you to read this JavaScript: Difference between .forEach() and .map() as well. You should never use .map like this. For this use case, use .forEach instead.
You would want to update the state with the minimum calls needed.
so first, I would do it like this:
let _total = 0;
array.forEach((item) => {
_total += item.amount;
});
setTotal(_total);
That said, You would want to only execute this if array has changed. Assuming array is a prop, this can be done easily with useEffect hook:
useEffect(()=>{
let _total = 0;
array.forEach((item) => {
_total += item.amount;
});
setTotal(_total);
},[array]);
Hope this helps you get a full picture of what the best practice would be. Also you can check out the rules of hooks to get a better understanding on where is best to call setState
My comment wasn't addressed but I'm going to add an answer which addresses my concern - total shouldn't be state at all.
total most likely isn't state - it's computed state - i.e. it's derived from other state and/or props.
If that's the case (99% that it is) it's not correct to set total as state, that just makes for more code and more complicated debugging:
Examples:
When the source of data is a prop:
const Cart = ({someItemsInMyCart}) => {
const total = useMemo(() => someItemsInMyCart.reduce((acc,item) => acc+item.amount,0),[someItemsInMyCart]);
return (/* some JSX */);
}
When the source of data is state:
const Cart = () => {
const [items,setItems] = useState([]);
const total = useMemo(() => items.reduce((acc,item) => acc+item.amount,0),[items]);
return (/* some JSX */);
}
You can write those two examples above and completely leave out the useMemo, which is just a perf optimization, because reducing an array in that manner is pretty darn fast unless you're dealing with 1000s of items.
Try this way
const [total, setTotal] = useState(0);
array.map((item) => {
setTotal(prevCount => prevCount + item.amount);
});
Related
I'm not sure if this is related to React or just JavaScript.
I'm building a simple voting app. You can add some options and vote +1 for each option.
My App has options as state like below. storedOptions is from localStorage.
function App() {
const [options, setOptions] = useState(
storedOptions ? JSON.parse(storedOptions) : []
);
And handleVote increases count by 1 for the given option.
const handleVote = useCallback((option) => {
setOptions((options) => {
console.log("previous: ", options);
let updatedOptions = [...options];
console.log(updatedOptions); // THIS PART IS STRANGE
const index = updatedOptions.indexOf(option);
console.log(index);
updatedOptions[index] = { ...option, count: option.count + 1 }; // Change reference of the given option only
console.log("new: ", updatedOptions);
updatedOptions = sortByValue(updatedOptions, "count"); // I think this is not related to my problem though, this is why I declared updatedOptions with 'let'. sortByValue function returns new array.
localStorage.setItem(OPTIONS_KEY, JSON.stringify(updatedOptions)); // I'm working with localStorage too, you can ignore this
return updatedOptions;
}, []);
});
But when I voted for an option, it didn't work as it supposed to be. So I logged them out like above and found out that console.log(updatedOptions)(second log) already reflected future operation(increasing count).
Shouldn't count be 0 at that moment? why 1 already?
screeen record of the issue: https://streamable.com/ofn42v
it is working fine in local but once deployed to production(vercel), it is not working. i have tried sooo many different things like having a separate state in cart, useEffect with totalQuantity in dependency array and nothing seems to work. Ideally when the totalQuantity inside the context is updated, the components using it should rerender as mentioned in react doc which is happening from n to 2 except for 1. can someone please help :(
my code for the cart icon in nav bar:
function Cart(props) {
const { enableCart, totalQuantity } = useContext(AppContext);
return (
<>
{enableCart ? (
<Link href="/cart" passHref>
<a aria-label="Shopping cart" title="Shopping cart">
<Badge count={totalQuantity} offset={[0, 5]}>
<ShoppingCartIcon className="w-7 h-7" />
</Badge>
</a>
</Link>
) : null}
</>
);
}
Update quantity - code in appContext:
import { useCookies } from "react-cookie";
export const AppProvider = (props) => {
const [cartItems, updateCart] = useState([]);
const [totalQuantity, setTotalQuantity] = useState(0);
const [cookies, setCookie] = useCookies(["cart"]);
const cookieCart = cookies.cart;
useEffect(() => {
cartOperations();
}, []);
const calculateAmountQuantity = (items) => {
let totalCount = 0;
let totalPrice = 0;
items.forEach((item) => {
totalCount += item.quantity;
totalPrice += item.price * item.quantity;
setTotalAmount(totalPrice);
setTotalQuantity(totalCount);
});
};
const cartOperations = async (items) => {
if (items !== undefined) {
updateCart([...items]);
calculateAmountQuantity(items);
} else if (cookieCart !== undefined) {
updateCart([...cookieCart]);
calculateAmountQuantity(cookieCart);
} else {
updateCart([]);
setTotalAmount(0);
setTotalQuantity(0);
}
};
const addItem = (item) => {
let items = cartItems;
let existingItem;
if (items) existingItem = items.find((i) => i.id === item.id);
if (!existingItem) {
items = [
...(items || []),
Object.assign({}, item, {
quantity: 1,
}),
];
updateCart([...items]);
setTotalAmount(totalAmount + item.price * 1);
setTotalQuantity(totalQuantity + 1);
} else {
const index = items.findIndex((i) => i.id === item.id);
items[index] = Object.assign({}, item, {
quantity: existingItem.quantity + 1,
});
updateCart([...items]);
setTotalAmount(totalAmount + existingItem.price);
setTotalQuantity(totalQuantity + 1);
}
saveCartToCookie(items);
saveCartToStrapi(items);
};
i am storing the cart content in cookie.
code for AppContext is here in github, full nav bar code
Live url: https://sunfabb.com
Goto Products, add few items to cart, then try removing one by one from the cart page. (i have enabled react profiler in prod as well)
EDIT: This issue is completely specific to antd library. I was able to debug further based on the below 2 answers and there is nothing wrong with react context or re-render. i tried using a custom badge for cart and it is working perfectly fine. Yet to fix the antd issue though. I can go with custom one, but antd's badge is better with some animations.
As pointed out by #hackape, when setting the value of state to something that depends on the previous value of that state, you should pass a function to the setState instead of a value.
So instead of setTotalQuantity(totalQuantity + 1);, you should say setTotalQuantity(previousQuantity => previousQuantity + 1);.
This is the safe way of doing that, so for example if we are trying to do it twice simultaneously, they both get taken into account, instead of both using the same initial totalQuantity.
Other thing that I would think about changing is that you are setting those quantities and amounts in multiple places, and relying on the previous value. So if it goes out of sync once, it's out of sync also on the next action, and so on.
You could use the useEffect hook for this. Every time the cartItems change, calculate those values again, and do that based only on the new cartItems array, not on the old values.
Something like this for example:
useEffect(() => {
setTotalAmount(cartItems.reduce((total, currentItem) => total + (currentItem.price * currentItem.quantity), 0));
setTotalQuantity(cartItems.reduce((total, currentItem) => total + currentItem.quantity, 0));
}, [cartItems]);
Or if you prefer calling it like you do now, I would still replace the value with the reduce from my example, so it get's calculated based on the whole cart instead of previous value.
A shopping cart is usually something that contains less than 100 entries, so there is really no need to worry about the performance.
From looking at the renders and from seeing that after a refresh the cart shows as empty as should be, it's probably a lifecycle issue.
I'd suggest creating another useEffect hook that listens to totalQuantity or totalAmount (logically the bigger of the two though by the state values it looks either should be fine) and in the hook call change the cart icon based on the updated sum
EDIT:
misread your inter-component imports, because Cart (from components/index/nav.js) should listen for changes from the context.provider you would use a context.consumer on Cart with the totalQuantity value (not just with importing the variable from the context as that rides on the application rendering from other reasons)
see example in consumer docs and in this thread, and check this GitHub issues page for other's detailed journey while encountering this issue more directly
I am trying to change the state by selecting and deselecting the language option in the code below. So far I can update the state by adding a language, my problem is that, if I click on the same language again, I will add it another time to the array. Can anyone explain me how to add or remove the language from the array when clicked one more time?
export default function Dashboard(props) {
const [language, setLanguage] = useState('');
const handleLanguageChange = changeEvent => {
changeEvent.persist()
setLanguage(prevState => [...prevState, changeEvent.target.value])
};
}
It looks like your only issue is your logic in the place where you are handling update. Usage of hooks is correct
So first of all you need to set proper initial value. As you plan to store your languages in an array.
Second part is updating the array. So you can either find clicked language in the array and if it is exist - then use filter to set your new value or filter and compare length of existing array and new one.
const [language, setLanguage] = useState([]);
const handleLanguageChange = changeEvent => {
changeEvent.persist()
setLanguage(prevState => {
const lang = changeEvent.target.value;
if (prevState.includes(lang) {
return prevState.filter(el => el !== lang);
}
return [...prevState, lang]
})
};
You will need a good old check.
if (!languages.includes(changeEvent.target.value) {
// code to add the language
}
Check the selected value using find() method on language array if it returns undefined, then push into array. Rename the state variable as languages otherwise it's confusing (Naming convention standard).
const [languages, setLanguages] = useState('');
const handleLanguageChange = changeEvent => {
changeEvent.persist()
if (!languages.find(value => value == changeEvent.target.value)) {
setLanguages(prevState => [...prevState, changeEvent.target.value])
}
};
2 Things here
Instead of having
<option value="Deutsch">Deutsch</option>
<option value="Englisch">Englisch</option>
use an languages array of json so it bacomes easy for you to add them like
languages= [{value='Deutsch',name= 'Deutsch',...}]
2.setLanguage sa a direct value
setLanguage(changeEvent.target.value)
I am trying to create a scoreboard for a quiz application. After answering a question the index is updated. Here is the code for the component.
export const ScoreBoard = ({ result, index }) => {
const [score, setScore] = useState(0)
const [total, setTotal] = useState(0)
const [rightAns, setRight] = useState(0)
useEffect(() => {
if(result === true ) {
setRight(rightAns + 1)
setTotal(total + 1)
}
if(result === false) {
setTotal(total + 1)
}
setScore(right/total)
}, [index]);
return (
<>
<div>{score}</div>
<div>{rightAns}</div>
<div>{total}</div>
</>
)
}
When it first renders the values are
score = NaN
rightAns = 0
total = 0
After clicking on one of the corrects answers the values update to
score = NaN
rightAns = 1
total = 1
and then finally after one more answer (with a false value) it updates to
score = 1
rightAns = 1
total = 2
Score is no longer NaN but it is still displaying an incorrect value. After those three renders the application begins updating the score to a lagging value.
score = 0.5
rightAns = 2
total = 3
What is going on during the first 3 renders and how do I fix it?
You shouldn't be storing the score in state at all, because it can be calculated based on other states.
All the state change calls are asynchronous and the values of state don't change until a rerender occurs, which means you are still accessing the old values.
export const ScoreBoard = ({ result, index }) => {
const [total, setTotal] = useState(0)
const [rightAns, setRight] = useState(0)
useEffect(() => {
if(result === true ) {
setRight(rightAns + 1)
setTotal(total + 1)
}
if(result === false) {
setTotal(total + 1)
}
}, [index]);
const score = right/total
return (
<>
<div>{score}</div>
<div>{rightAns}</div>
<div>{total}</div>
</>
)
}
Simpler and following the React guidelines about the single "source of truth".
Your problem is that calling setState doesn't change the state immediately - it waits for code to finish and renders the component again with the new state. You rely on total changing when calculating score, so it doesn't work.
There are multiple approaches to solve this problem - in my opinion score shouldn't be state, but a value computed from total and rightAns when you need it.
All of your set... functions are asynchronous and do not update the value immediately. So when you first render, you call setScore(right/total) with right=0 and total=0, so you get NaN as a result for score. All your other problems are related to the same problem of setScore using the wrong values.
One way to solve this problem is to remove score from state and add it to the return like this:
return (
<>
{total > 0 && <div>{right/total}</div>}
<div>{rightAns}</div>
<div>{total}</div>
</>
)
You also can simplify your useEffect:
useEffect(() => {
setTotal(total + 1);
if(result === true ) setRight(rightAns + 1);
}, [index]);
With how you have it set up currently, you'd need to make sure that you are updating result before index. Because it seems like the useEffect is creating a closure around a previous result and will mess up from that. Here's showing that it does work, you just need to make sure that result and index are updated at the right times.
If you don't want to calculate the score every render (i.e. it's an expensive calculation) you can useMemo or useEffect as I have shown in the stackblitz.
https://stackblitz.com/edit/react-fughgt
Although there are many other ways to improve how you work with hooks. One is to make sure to pay attention to the eslint react-hooks/exhaustive-deps rule as it will forcefully show you all the little bugs that can end up happening due to how closures work.
In this instance, you can easily calculate score based on total and rightAns. And total is essentially just index + 1.
I'd also modify the use effect as it is right now to use setState as a callback to get rid of a lot of dependency issues in it:
useEffect(() => {
if (result === true) {
setRight(rightAns => rightAns + 1);
setTotal(total => total + 1);
}
if (result === false) {
setTotal(total => total + 1);
}
}, [index]);
useEffect(()=>{
setScore(rightAns / total ||0);
},[rightAns,total])
I've got a reducer that has a data attribute that is an array of objects. That is, basically:
state.data[0] = {id: 1,name: 'joe',tired=true}
state.data[1] = {id: 2,name: 'linda',tired=false}
etc.
I've found that in my reducer, if I want to make linda not tired, I have to dig really deep to force the react "differ" engine recognize a state chage happened. As you can see by the below code, I've practically create a new reference to everything.
Is there a simpler way to do this? I wish I understood how the diff works better so that my object gets rendered when I set the attribute tired to true for a given row. It feels like I'm just thrashing everything.
const idToUpdate = 2;
newState = Object.assign({}, state);
let newData = [];
newState.data.map(function(rec){
if (rec.id === idToUpdate) {
rec.interestLevel = 998;
newData.push(rec);
} else {
newData.push(rec);
}
});
newState.data = newData;
if you know the id you want to update and im assuming you have an array of objects then you can do something like
const {data} = this.state;
const arr = data;
const Linda = arr.filter(item => item.id === idToUpdate)
var TiredLinda = Linda.map(item => return {id:item.id, name:item.name, tired:true}
//Now remove Linda from the original array
arr.filter(item => item.id !== idToUpdate)
//Now we will push the updated Linda to arr to replace the one we removed
arr.push(TiredLinda);
Now you want to set the state of your data
this.setState({data:arr});