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>;
}
Related
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?
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>
)
}
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;
}
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)
I am building a react project for visualizing insertion sort using redux. I am using react-redux to create and handle actions. However, the problem is that in my insertionSort algorithm, I dispatch an updateArray action every time the array being sorted changes. I put print statements inside the reducer and saw that the state was in fact changing and the action was being dispatched correctly, however, my actual array does not re-render. I put prints inside the relevant UI component's render() function and saw that it was only being called once or twice rather than every time the reducer receives the action. I tried restructuring my code multiple times and reading about similar problems that people have had but their answers did not help me.
Am I just structuring this the wrong way? Should I not be using dispatches every second or so to update my array?
I have a main.js file which is used to render the UI components including my array:
class Main extends React.Component {
setArray = () => {
this.props.setArray(50, window.innerHeight / 1.4)
startSort = () => {
this.props.startSorting(this.props.algorithm, this.props.array)
}
render() {
let { array} = this.props
return (
<div>
<Navbar
startSort={this.startSort}
setArray={this.setArray}
/>
<MainWrapper>
<Row />
</MainWrapper>
</div>
)
}
}
const mapStateToProps = state => {
return {
array: state.array,
}
}
const mapDispatchToProps = dispatch => {
return {
setArray: (length, height) => {
let array = Array.from({ length: length }, () =>
Math.floor(Math.random() * height))
dispatch(setArray(array))
},
startSorting: (algorithm, array) => {
var doSort
if (algorithm == 'insertionSort') {
doSort = insertionSort
}
doSort(array, dispatch)
}
}
}
My actual array is generated with Row.js
class Row extends React.Component {
generateNodes(array) {
var elements = []
array.forEach((value, index) => {
elements.push(
<CenteredColumn>
<ArrayNode idx={index} value={value} />
</CenteredColumn>
)
})
return elements
}
render() {
let { array } = this.props
console.log('UPDATED ARRAY: ' + array)
var arrayElements = this.generateNodes(array)
return <RowWrapper>{arrayElements}</RowWrapper>
}
}
const mapStateToProps = state => {
return {
array: state.array
}
}
And finally, my actual algoritm is in insertionSort.js in which I import my actions from their reducers and pass in a dispatch function from main.js:
function delayedInsertion(array, dispatch) {
let n = array.length
var i = 0
function loop() {
setTimeout(function() {
var temp = array[i]
var j = i - 1
while (j >= 0 && array[j] > temp) {
array[j + 1] = array[j]
j--
}
array[j + 1] = temp
// console.log('ARRAY: ' + array)
dispatch(updateArray(array))
i++
if (i < n) {
loop()
}
}, 200)
}
loop()
console.log('DONE')
}
It seems that you are mutating your state.
You are passing this.props.array to your doSort action and as I understand your idea correctly, you are just calling delayedInsertion from that action (you did not post source code of that action).
But in delayedInsertion you are mutating the passed array when you are changing positions of you items, here:
while (j >= 0 && array[j] > temp) {
array[j + 1] = array[j]
j--
}
array[j + 1] = temp
You need to perform immutable change of the array.