React Function is not using updated array? - javascript

I am learning React and am writing a React App that lets you press buttons that have values (1, 0, -1) and then does some calculations (average, %pos, etc).
I have written a function to calculate the average in my App.js component
const [good, setGood] = useState(0)
const [neutral, setNeutral] = useState(0)
const [bad, setBad] = useState(0)
const [allNum, setAll] = useState([])
const [average, setAverage] = useState(0)
const calcAverage = () => {
console.log('values in calcAvg for allNum: ', allNum)
let total = 0;
for(let i = 0; i < allNum.length; i++) {
total += allNum[i];
}
return total/allNum.length;
}
const handleGoodClick = () => {
setGood(good + 1)
setAll(allNum.concat(1))
setAverage(calcAverage());
}
const handleNeutralClick = () => {
...
}
const handleBadClick = () => {
...
}
return(
<div>
<h1>Give Feedback</h1>
<Button handleClick={handleGoodClick} text="good"/>
...
<Statistics good={good} neutral={neutral} bad={bad} allNum={allNum} average={average}/>
</div>
)
}
The Statistics component is as follows:
const Statistics = (props) => {
console.log('props value is: ', props)
return(
<div>
<h1>Statistics</h1>
...
<Statistic text="Average" value={props.average}/>
</div>
)
}
When I press a button and the app attempts to calculate the average, the average array is always 1 value behind.
ie, open app, press good, Average shows as NaN, console.log shows the allNum array containing a 1 but when Average is calculated that 1 is not contained in the allNum array yet. Why doesn't it go in order? How can I make it execute in order? What is the best practice approach?
Thanks

The reason the calculations receive the previous values is because the setState functions provided by useState do not set the state synchronously.
To overcome this you can use something like a useEffect to update average whenever allNum changes (reference comparison).
const MiscComponent = () => {
const [good, setGood] = useState(0)
const [neutral, setNeutral] = useState(0)
const [bad, setBad] = useState(0)
const [allNum, setAll] = useState([])
const [average, setAverage] = useState(0)
// The useEffect will trigger based on what is entered into the
// dependency array: [setAverage, allNum]
// Each render cycle the values will be compared (reference comparison)
// to the previous values in the dependency array and if there is a
// change the effect will be run.
// NOTE: There is always considered to be a "change" on component mount.
// setAverage is guarenteed to never change reference once created by useState.
// Someone else probably has the link to the React docs for this statement.
// So everytime allNum is updated this effect should run.
useEffect(() => {
const average = allNum.reduce((a, b) => a + b, 0) / allNum.length;
setAverage(average)
}, [setAverage, allNum])
// Since the average update will be handled by the useEffect it can
// now be removed from the click handler.
const handleGoodClick = () => {
setGood(good + 1)
setAll(allNum.concat(1))
}
const handleNeutralClick = () => {
/* Code here */
}
const handleBadClick = () => {
/* Code here */
}
return(
<div>
<h1>Give Feedback</h1>
<Button handleClick={handleGoodClick} text="good"/>
{/* Components Here */}
<Statistics good={good} neutral={neutral} bad={bad} allNum={allNum} average={average}/>
</div>
)
}
Another option if average is only ever effected (affected? whatever) by other state variables you could use a useMemo instead.
const MiscComponent = () => {
const [good, setGood] = useState(0)
const [neutral, setNeutral] = useState(0)
const [bad, setBad] = useState(0)
const [allNum, setAll] = useState([])
// useMemo will return a memoized value for average that will only be recalculated
// based on its associated dependency array.
const average = useMemo(() => {
const average = allNum.reduce((a, b) => a + b, 0) / allNum.length;
return average;
}, [allNum]);
// Since the average update will be handled by the useMemo it can
// now be removed from the click handler.
const handleGoodClick = () => {
setGood(good + 1)
setAll(allNum.concat(1))
}
const handleNeutralClick = () => {
/* Code here */
}
const handleBadClick = () => {
/* Code here */
}
return(
<div>
<h1>Give Feedback</h1>
<Button handleClick={handleGoodClick} text="good"/>
{/* Components Here */}
<Statistics good={good} neutral={neutral} bad={bad} allNum={allNum} average={average}/>
</div>
)
}

Related

Uncaught TypeError: Cannot read properties of undefined (reading '0'), Summing useState arrays

const [lnames, setlNames] = React.useState();
const [lnums, setlNums] = React.useState();
React.useEffect(() => {
axios.get("http://localhost:7001/lunch").then(response => {
let arr1 = [];
let arr2 = [];
response.data.forEach(c => {
arr1.push(c.table_id);
arr2.push(c.table_num_count);
});
setlNames(arr1);
setlNums(arr2);
});
}, []);
const [dnums, setdNums] = React.useState();
React.useEffect(() => {
axios.get("http://localhost:7001/dinner").then(response => {
let arr1 = [];
response.data.forEach(c => {
arr1.push(c.table_num_count);
});
setdNums(arr1);
});
}, []);
const [bnums, setbNums] = React.useState();
React.useEffect(() => {
axios.get("http://localhost:7001/breakfast").then(response => {
let arr1 = [];
response.data.forEach(c => {
arr1.push(c.table_num_count);
});
setbNums(arr1);
});
}, []);
const customer_count_breakfast = bnums;
const customer_count_lunch = lnums;
const customer_count_dinner = dnums;
let sumArray = []
if (lnums & lnums.length > 0) {
//sumArray = lnums.map((l, i) => l + bnums[i] + dnums[i]);
for (let i = 0; i < lnums.length; i++) {
sumArray[i] = bnums[i] + lnums[i] + dnums[i];
}
}
// IF COMMENT ABOVE AND UNCOMMENT HERE THE ERROR IS GONE BUT sumArray is not computed
// if (lnums && lnums[0]) {
// //sumArray = lnums.map((l, i) => l + bnums[i] + dnums[i]);
// for (let i = 0; i < lnums.length; i++) {
// sumArray[i] = bnums[i] + lnums[i] + dnums[i];
// }
// }
Hello I have the above code giving me the following error at the if loop condition : Uncaught TypeError: Cannot read properties of undefined (reading '0'). I think I know why the error is being displayed. It is indicating that I am trying to access a property or index of a variable that is undefined, this is most likely due to the useEffect, where when I call for the if condition the value is not set yet, so it is undefined and thus does not have a length. When I try to comment the first if condition and uncomment the second, I get no more errors but the summation does not work, it returns an empty array.
How would I be able to sum the values of .useState() variables in this case?
You need to look into react lifecycle and useMemo.
read about lifecycle: https://www.w3schools.com/react/react_lifecycle.asp
read about useMemo: https://www.w3schools.com/react/react_usememo.asp
Here is a quick example where useEffect puts numbers into an array, useMemo has dependency on those arrays and adds all the numbers together.
import { useEffect, useState, useMemo } from "react";
export default function App() {
const [arr1, setArr1] = useState([]);
const [arr2, setArr2] = useState([]);
// called everytime arr1 or arr2 changes. Which causes react to rerender.
// first run sum = 0
const sum = useMemo(() => {
let res = 0;
if (arr1.length && arr2.length) {
for (let i = 0; i < arr1.length; i++) {
res += arr1[i] + arr2[i];
}
}
return res;
}, [arr1, arr2]);
// called once, updated setArr1 causing sum to be called
useEffect(() => {
setArr1([1,2,3]);
}, []);
// called once, updated setArr2 causing sum to be called
useEffect(() => {
setArr2([4,5,6]);
}, []);
return (
<div>
<p> sum: {sum} </p>
</div>
);
}
Essentially In your code you are calling the arrays before useEffect has finished setting their values.
I solved the issue by adding a try-catch statement, not sure if it is feasible at all
try {
for (let i = 0; i < lnums.length; i++) {
sumArray[i] = bnums[i] + lnums[i] + dnums[i];
}
console.log("Statement was successful");
} catch (error) {
}

Why is Javascript closure value apparently captured?

const MyComponent = () => {
const history = useHistory();
let [val, setVal] = useState(0);
useEffect(() => {
trackPageView();
history.listen(trackPageView);
}, []);
function trackPageView() {
console.log('hello: ' + val);
setVal(val + 1);
}
}
useEffect runs once and registers trackPageView with history.listen. As the history changes, trackPageView is called as expected but val always has the same start value of '0'. (The above code is based on the code in this article.)
In contrast, this code does a similar thing...
var a = 1;
function g(f) {
return () => f();
}
let func = g(() => console.log('a: ' + a));
func();
a = 2;
func();
...but a's changing value is reflected in the output. https://jsfiddle.net/s3e250da/
So, what accounts for the difference in behaviour?

React cannot handle counter properly

I have a simple code which iterates through an array and logs them in the interval of 1000 ms as such :
const arr = [1, 2, 3];
let i = 0;
const choice = () => {
const interval = setInterval(() => {
console.log(arr[i++ % arr.length]);
if (i === 8) {
clearInterval(interval);
}
}, 1000);
};
choice();
Upon introducing React state into the code, the whole thing goes absolutely mad and starts counting out of the interval to a point that I reach almost infinite loop although the simple console.log instead of react state works fine.
const [ele, setEle] = React.useState(null);
const arr = [1, 2, 3];
let i = 0;
const choice = () => {
const interval = setInterval(() => {
setEle(arr[i++ % arr.length]);
if (i === 8) {
clearInterval(interval);
}
}, 1000);
};
choice();
return(
<h1>{ele}</h1>
)
I wonder how I can achieve the effect using the state with the current code.
https://codesandbox.io/s/condescending-dubinsky-kbl5m
With your current implementation, every render of the component is initializing a new interval, so you get lots of intervals running simultaneously, each of which initiate even more intervals. Use setTimeout instead, so that every render only initializes exactly one action to take place in the future.
Since it's the index of the array that changes each time, consider using that as state instead:
const App = () => {
const [i, setI] = React.useState(0);
const arr = [1, 2, 3];
if (i !== 8) {
setTimeout(() => {
setI(i + 1);
}, 1000);
}
return (
<div className="App">
<h1>{arr[i % arr.length]}</h1>
</div>
);
}
ReactDOM.render(
<App />,
document.body
);
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
I found the other answer somewhat unsatisfying because it requires a new timeout to be generated each time. It is possible to set up a single setInterval which requires a useRef to get an up-to-date setI into the timer. The empty useEffect dependencies ensures the setInterval is not re-run on each state update.
const App = () => {
const [i, setI] = React.useState(0);
const timer = React.useRef();
timer.current = () => setI(i+1);
React.useEffect(() => {
const interval = setInterval(() => {
timer.current();
}, 1000);
return () => {
clearInterval(interval)
};
}, [])
return <div>{i}</div>;
}

How to properly deconstruct an array of arrays using hooks in react

I'm trying to turn a long list of objects (accounts) into smaller arrays of 6 or less. I'm doing this because I have a table in my react component that is listing all the accounts, however, it cannot hold any more than 6 accounts. I created the pagination and everything, and thought that it would work, but the current page doesn't ever seem to update.
const [pageNum, setPageNum] = useState(1);
const [numOfPages, setNumOfPages] = useState(Math.ceil(accounts.length / 6));
const [page, setPage] = useState([]);
const [pages, setPages] = useState([null]);
const onClick = (e) => {
setPageNum(pageNum + 1);
setPage(pages[pageNum]);
};
useEffect(() => {
if (numOfPages > 1) {
for (let i = 0; i < 6; i++) {
setPages(pages.push(accounts.slice(i * 6, i * 6 + 6)));
}
}
console.log(pages[1][1]);
console.log(pages[2][2]);
setPage([pages[1][1], pages[1][2]]);
console.log(page);
}, []);
This isn't the final code, but rather part of my troubleshooting and I also think its where the issue is coming up. When I console log pages[1][1] and pages[1][2], I get two separate objects. So how come after using setPage and placing the two 'objects' inside of an array, does the console log of page come out as an empty array?? I've tried refactoring so many things. I've changed pages to an object instead of an array, i've tried doing it in another method instead of useEffect, all kinds of things. But the root of the issue seems to be that often my 'setPage' and my 'setPageNum' methods fail for seemingly no reason.
Prepare the pages array with useMemo (it will only change when accounts changes).
const { useState, useMemo, useCallback } = React;
const Pager = ({ accounts }) => {
const [pageNum, setPageNum] = useState(0);
const pages = useMemo(() => {
const pages = [];
for (let i = 0; i < accounts.length; i += 6) {
pages.push(accounts.slice(i, i + 6));
}
return pages;
}, [accounts]);
// console.log(pages);
const numOfPages = pages.length;
const page = pages[pageNum];
const next = useCallback(() => {
setPageNum(pageNum => pageNum + 1);
}, []);
const prev = useCallback(() => {
setPageNum(pageNum => pageNum - 1);
}, []);
return (
<div>
<button disabled={pageNum === 0} onClick={prev}>Prev</button>
<button disabled={pageNum === numOfPages - 1} onClick={next}>Next</button>
<ul>
{pages[pageNum].map((s, i) => <li key={i}>{s}</li>)}
</ul>
</div>
);
};
const accounts = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
ReactDOM.render(
<Pager accounts={accounts} />,
root
)
<script crossorigin src="https://unpkg.com/react#16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.development.js"></script>
<div id="root"></div>
The main problem in your code is the way you use setPages:
setPages(pages.push(accounts.slice(i * 6, i * 6 + 6)));
The Array.push() returns the new length of the array. So your pages state is actually the last number. It's the last number because setState replaces the state, and doesn't add it.
The term you are looking for is chunk.
Since the data you are chunking comes from props, you don't need chunk inside the useEffect hook. It's not a side effect.
Here is a chunk function I found on the internet:
function chunkArray(myArray, chunk_size){
var index = 0;
var arrayLength = myArray.length;
var tempArray = [];
for (index = 0; index < arrayLength; index += chunk_size) {
myChunk = myArray.slice(index, index+chunk_size);
// Do something if you want with the group
tempArray.push(myChunk);
}
return tempArray;
}

Adding numbers from array in useEffect

I am trying to use hooks to add numbers from an array together. Currently it's a voting system. The result of adding all the numbers using a map statement gives me 0. I am pretty sure this has to do with useState not updating in time to add the numbers, therefore it's always giving me zero. I know I could put them in a separate array, and add that, but that seems a little verbose for something that would seem so simple.
Here is the code I have that produces 0
const PollResultsContainer = (props) => {
const option = props.option
const [totalVotes, setTotalVotes] = useState(0)
useEffect(() => {
let newVote
if (option.length > 0) {
option.map(opt => {
newVote = opt.optionVotes + totalVotes
})
}
setTotalVotes(newVote)
}, [totalVotes])
console.log(totalVotes)
return (
<>
<div className='poll-results-div'>
<TitleCardNS title={`${totalVotes}`} size='2.5rem' align='center' color='white' />
</div>
There is not need to store it in a state.
const PollResultsContainer = ({option}) => {
let totalVotes = option.reduce((acc, {optionVotes}) => acc + optionVotes, 0);
console.log(totalVotes);
};
I don't think you need any state or effect in this component :
const PollResultsContainer = (props) => {
const option = props.option
let totalVotes = 0;
if (option.length > 0) {
option.forEach(opt => {
totalVotes += opt.optionVotes
})
}
console.log(totalVotes)
I added another variable into the equation. This probably solved the problem of useState not updating in time.
const PollResultsContainer = (props) => {
const option = props.option
const [totalVotes, setTotalVotes] = useState(0)
let newVote = 0
useEffect(() => {
if (option.length > 0) {
option.map(opt => {
newVote = opt.optionVotes + newVote
console.log(newVote)
})
}
console.log(newVote)
setTotalVotes(newVote)
}, [totalVotes])
console.log(totalVotes)

Categories