How to update state using settimeout function inside loop in react? - javascript

I'm creating react app to visualize sorting arhorithms and I stopped on this problem. I'm looping through all elements of array from bars state and I want to swap them (for test purposes). This is not working excatly as I wanted, because it "ignores" setTimeout function and does it immediately. I tried something with setBars but it is not working too. How can I make this so swapping will happen after timeout set in setTimeout function?
const [bars, setBars] = useState([23, 63, 236, 17, 2]);
const swap = (arr, i, j) => {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
};
for (let i = 0; i < bars.length - 1; i++) {
setTimeout(() => {
swap(bars, i, i + 1);
}, 2000);
}

You'd want to use a useEffect hook to start the process when the component mounts:
useEffect(() => {
// ...code here...
}, []);
The empty dependencies array at the end says "only run this when the component first mounts."
Within it, your code would schedule the timer callbacks:
for (let i = 0; i < bars.length - 1; i++) {
setTimeout(() => {
setBars(bars => {
// Copy the current array
const newBars = [...bars];
// Do the swap
swap(newBars, i, i + 1);
// Set the state by returning the update
return newBars;
});
}, 2000 * (i + 1));
}
Note that that uses the callback form of setBars, so that you're operating on the then-current state (instead of bars, which is only the array used during the first mount, since your function is only called when that first mount occurs).
Also note that the interval is 2000 * (i + 1) rather than just 2000. That's so each callback occurs 2000ms after the last one, since they're all being scheduled at once.
Another important aspect of the above is that it uses let within the for, so each loop body gets its own i variable. (If the code used var, or let outside the for, all the callbacks would share the same i variable, which would have the value bars.length.)
Alternatively, I think I might take this approach:
Since you want to update both i and bars at the same time and since it's best to use the callback form of the state setter when doing updates asynchronously (as with setTimeout), I'd combine i and bars into a single state item (whereas I'd normally keep them separate):
const [barState, setBarState] = useState({i: 0, bars: [23, 63, 236, 17, 2]});
Then you'd use a useEffect hook to kick the process off when your component mounts and then do it again after each change to barState:
useEffect(() => {
// ...code here...
}, [barState]);
The dependencies array containing barState at the end says "run this on component mount and every time barState changes."
Within it, your code would schedule the timer callbacks:
if (barState.i < barState.bars.length - 1) {
setTimeout(() => {
setBarState(({i, bars}) => {
// Copy the current array
bars = [...bars];
// Do the swap
swap(bars, i, i + 1);
// Set the state by returning the update
++i;
return {i, bars};
});
}, 2000);
}
Live Example:
const { useState, useEffect } = React;
const swap = (arr, i, j) => {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
};
function Example() {
const [barState, setBarState] = useState({i: 0, bars: [23, 63, 236, 17, 2]});
useEffect(() => {
if (barState.i < barState.bars.length - 1) {
setTimeout(() => {
setBarState(({i, bars}) => {
// Copy the current array
bars = [...bars];
// Do the swap
swap(bars, i, i + 1);
// Set the state by returning the update
++i;
return {i, bars};
});
}, 2000);
}
}, [barState]);
return <div>{barState.bars.join(", ")}</div>;
}
ReactDOM.render(<Example />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>

Related

React 'Cannot set properties to undefined': expensive computation?

I have an issue while rendering the following.
I have a useBoard hook, which should create a new board: a 2d Array, which every cell is an object, in which some of these object have a value between 1-3 included, and the other 'empty cells' have the 0 value.
Cell object:
{
value: 0,
className: ''
}
My useBoard simply call the function to create the board.
export const useBoard = ({ rows, columns, pieces }: IBoardItems) => {
const [board, setBoard] = useState<ICell[][]>();
useEffect(() => {
const getBoard = () => {
const newBoard = createBoard({ rows, columns, pieces });
setBoard(newBoard);
}
getBoard();
}, [])
return [board] as const;
};
And here is the utility function to build it.
export const createBoard = ({ rows, columns, pieces }: IBoardItems) => {
let randomIndexes: number[][] = [];
while (randomIndexes.length < pieces) {
let randomIndex = [getRandomNumber(5, rows), getRandomNumber(0, columns)];
if (randomIndexes.includes(randomIndex)) {
continue;
} else {
randomIndexes.push(randomIndex);
};
};
let board: ICell[][] = [];
for (let i = 0; i < rows; i++) {
board.push(Array(columns).fill(defaultCell));
};
randomIndexes.forEach(([y, x]) => {
board[y][x] = {
value: getRandomNumber(1, 3),
className: ''
}
});
return board;
};
The fact is, that when I click start in my related component, which should render the <Game /> containing the <Board />, sometimes it works, sometimes not, and the console logs the error 'Cannot set properties to undefined'. I think I understand but I'm not sure: is it because could happen, that in some steps, some data is not ready to work with other data?
So, in this such of cases, which would be the best approach? Callbacks? Promises? Async/await?
NB. Before, I splitted the createBoard function into smaller functions, each one with its owns work, but to understand the issue I tried to make only one big, but actually I would like to re-split it.
EDIT: I maybe found the issue. In the createBoard I used getRandomNumber which actualy goes over the array length. Now the problem no longer occurs, but anyway, my question are not answered.

useEffect received a final argument during this render

I am getting this warning:
Warning: useEffect received a final argument during this render, but not during the previous render. Even though the final argument is optional, its type cannot change between renders.
warning screenshot here
//importing ...
const Relevance = () => {
const { data, setData } = useContext(defaultData);
if (data.length > 0) {
const Relevance = data.map((d) => {
return d.relevance;
});
const maxValue = Math.max(...Relevance);
let HashRelevance = new Array(maxValue + 1).fill(0);
for (let i = 0; i < Relevance.length; i++) {
if (Relevance[i] !== null) {
HashRelevance[Relevance[i]]++;
}
}
var keyRelevance = [];
var valueRelevance = [];
for (let i = 0; i < HashRelevance.length; i++) {
if (HashRelevance[i] !== 0) {
keyRelevance.push(i);
valueRelevance.push(HashRelevance[i]);
}
}
}
const [keyRelevanceState, setKeyIntenstityState] = useState([]);
const [valueRelevanceState, setValueIntenstityState] = useState([]);
useEffect(() => {
setKeyIntenstityState(keyRelevance);
}, keyRelevance);
useEffect(() => {
setValueIntenstityState(valueRelevance);
}, valueRelevance);
if (valueRelevanceState !== undefined && valueRelevanceState.length > 0) {
return (
returning component...
)
};
export default Relevance;
What is the error about?
That error is telling you that you're not passing a consistent value to useEffect as its final (second) argument (the optional dependency array) between renders. The dependency array argument you pass useEffect must either always be an array of dependencies (with the same length), or always be undefined (because you left it off, usually), on every render of your component.
You have this code:
if (/*...a condition...*/) {
// ...
var keyRelevance = [];
// ...
}
// ...
useEffect(() => {
// ...
}, keyRelevance);
That's functionally identical to:
var keyRelevance; // ***
if (/*...a condition...*/) {
// ...
keyRelevance = []; // ***
// ...
}
// ...
useEffect(() => {
// ...
}, keyRelevance);
That means you're declaring a keyRelevance variable that will initially have the value undefined, and then only assigning an array to it if a condition is met. Then you're using keyRelevance as the dependencies array.
The second argument to useEffect should be an array of dependencies, not just a single dependency, so wrap keyRelevance in an array:
useEffect(() => {
setKeyIntenstityState(keyRelevance);
}, [keyRelevance]);
// ^ ^
More in the documentation.
But separately, since you're creating a new array every time, that would cause the effect to change every time. Instead, memoize the creation of keyRelevance (and similar) using useMemo or useRef (since useMemo's documentation says it's just for performance optimization, and in your case you need to memoize the value for correctness, not just optimization).
For instance:
const relevanceInfo = useRef(null);
// (Probably put this condition in a function)
if (!relevanceInfo ||
data.length !== relevanceInfo.sourceData.length ||
data.some((d, index) => d.relevance !== relevanceInfo.sourceData[index].relevance)) {
relevanceInfo.keyRelevance = /* ...build the array... */;
relevanceInfo.sourceData = data;
}
// ...
useEffect(() => {
// ...
}, [relevanceInfo.keyRelevance]);
...or similar.
Side note: I wouldn't declare the variable inside the if block and then use it outside the if block. Instead, declare it in the top scope where it'll be used (in this case function scope). (Also, I suggest using let and const, not var; var is effectively deprecated and shouldn't be used in new code.)
Final value, (second parameter) of the useEffect should be in array of different values or empty array. You are providing a valueRelevance as final value of useEffect which itself an array but not in array , like below.
useEffect(() => {
setKeyIntenstityState(keyRelevance);
}, [valueRelevance]);
Or,
useEffect(() => {
setKeyIntenstityState(keyRelevance);
}, [param1, param2]);

Delete All Array Elements With Animation

I have a react native todo list app.
The task items are represented by an array using useState which is declared as follows:
const [taskItems, setTaskItems] = useState([]);
I'm trying to add a function that deletes all of the items but instead of just setting it like this setTaskItems([]), which simply deletes all items at once, I was hoping to delete the items one at a time starting from the end to create a more animated look.
while(taskItems.length > 0)
{
alert(taskItems.length);
let itemsCopy = [...taskItems];
itemsCopy.splice(-1);
setTaskItems(itemsCopy);
//setTimeout(function(){},180);
}
For some reason the above code causes infinite recursion. If I remove the while loop then the deleteAll function does indeed remove the last item in the list. Based on my placing the alert at the beginning of the function it seems like the length of the array is never decreased.
Why is that?
And is there a better way to achieve this effect?
It happen because in your code taskItem value doesn't change when you use setTaskItems
setTaskItems is an asynchronous function and the updated value of the state property change after the rerender of the component
you can try something like this
const [taskItems, setTaskItems] = useState([]);
const [deleteAll, setDeleteAll] = useState(false)
useEffect(() => {
if(!deleteAll) return;
if(taskItems.lenght === 0){
setDeleteAll(false)
return;
}
setTimeout(() => {
setTaskItems(t => t.slice(0, -1))
}, 180)
}, [deleteAll, taskItems])
const handleDeleteAll = () => {
seDeleteAll(true)
}
That is because taskItems value is retrieved from closure, meaning you can not expect value to be changed during handler execution. Closure recreates only after element rerenders, meaning you can access new state value only in next function execution - during the function execution value of the state never change.
You just need to assign length value to some temp variable, and use it as start/stop indicator for while loop:
let itemsCount = taskItems.length;
while(itemsCount > 0)
{
// Here use itemsCount to determine how much you would need to slice taskItems array
itemsCount -= 1;
}
UPDATED
An idea of how you should do it is:
const [items, setItems] = useState([1, 2, 3, 4, 5]);
const deleteAll = () => {
let counter = items.length;
while (counter > 0) {
setTimeout(() => {
setItems((p) => [...p.slice(0, counter - 1)]);
}, counter * 1000);
counter -= 1;
}
};
It is important to multiply timeout delay with length, in order to avoid simultaneous execution of all timeouts - you need one by one to be deleted.
Also you should clear timeout on unmount or something like that.
Due to asynchronous nature of React useState hook, you need to harness useEffect to do the job. Here's working example of removing all tasks with a 1000 ms delay between items.
function TasksGrid() {
const [taskItems, setTaskItems] = useState([1, 2, 3, 4, 5, 6]);
const [runDeleteAll, setRunDeleteAll] = useState(false);
useEffect(
() => {
if (runDeleteAll) {
if (taskItems.length > 0)
setTimeout(() => setTaskItems(taskItems.slice(0, taskItems.length - 1)), 1000);
else
setRunDeleteAll(false);
}
},
[runDeleteAll, taskItems]
)
const handleOnClick = () => {
setRunDeleteAll(true);
}
return (
<div>
{taskItems.map((taskItem, idx) => <p key={idx}>task {taskItem}</p>)}
<button onClick={handleOnClick}>delete all</button>
</div>
);
}

Function doesn't wait react hooks

I'm trying to add items in shopping cart. It works, but after adding items when I want to calculate number of items to show on the shopping cart. Second function (calculate()) doesn't wait items hooks. Because of that, it shows the correct count after adding second item.
Below code is my functions. As you can see, in the end of first function I'm calling the calculate() function to keep it continue.
const [testArray, setTestArray] = useState([]);
const [total, setTotal] = useState(0);
const [cartCount, setCartCount] = useState(0);
function addToTest(product, quantity = 1) {
const ProductExist = testArray.find((item) => item.id === product.id);
if (ProductExist) {
setTestArray(
testArray.map((item) => {
if (item.id === product.id) {
return { ...ProductExist, quantity: ProductExist.quantity + 1 };
} else {
return item;
}
})
);
} else {
product.quantity = 1;
setTestArray([...testArray, product]);
}
calculate();
}
function calculate() {
let resultCount = 0;
testArray.map((item) => {
console.log("map func works!");
setCartCount(cartCount + item.quantity);
});
}
Here is my codesandbox project, I kept it very simple to not bother you.
https://codesandbox.io/s/react-template-forked-u95qt?file=/src/App.js
The possible problem occurs due to synchronise functions. Because of that, when I try to async/await, I'm getting error about parameters, because the first function has parameter.
This is my first try for async/await:
async function calculate(product) {
await addToTest(product);
let resultCount = 0;
testArray.map((item) => {
console.log("map func works!");
setCartCount(cartCount + item.quantity);
});
}
As other solution I tried to use useEffect by taking the reference setArray hooks. However, in this case, the count number increases exponentially like 1,3,9...
useEffect(()=>{
let resultCount = 0;
testArray.map((item) => {
console.log("map func works!");
setCartCount(cartCount + item.quantity);
});
},[testArray])
I wonder where is the problem? Because when I use the the upper code in Angular/Typescript, it works properly. I think this happens due to react hooks, but I couldn't understand the problem.
Your thought to use a useEffect is a good one, but instead of calling setCartCount on every iteration you should instead sum all the item.quantity counts first and then call setCartCount once after your loop.
Note: don't use map() if you're going to ignore the array it returns, use a for loop, for...of as in the example below, or forEach() instead.
useEffect(() => {
let resultCount = 0;
for (const item of testArray) {
resultCount += item.quantity;
}
setCartCount(resultCount);
}, [testArray]);
or with reduce() (here destructuring and renaming the quantity property of each passed item)
useEffect(() => {
const resultCount = testArray.reduce((a, { quantity: q }) => a + q, 0);
setCartCount(resultCount);
}, [testArray]);
Every call to setCartCount will only update cartCount on the next render, so you are (effectively) only calling it with the final item in the array. You should instead use reduce on the array to get the value you want, and then set it.
See https://codesandbox.io/s/react-template-forked-u95qt?file=/src/App.js

How to increment a state variable every second in React

I have a list of images in my code for this component called "url". I also have a state variable called currImageIndex. What I want to happen is every time this component is rendered, I would like to start a timer. Every second this timer runs, I would like to increment my state variable currImageIndex. When currImageIndex + 1 is greater than or equal to url length, I would like to reset currImageIndex to zero. The overall goal is to simulate an automatic slideshow of images by just returning <img src={url[currImageIndex]}>. However, I am having issues with incrementing my state variable. I followed some code I found on here, but I can't figure out how to get the increment working. Here is my code.
const { url } = props;
const [currImageIndex, setCurrImageIndex] = useState(0);
console.log(url)
const increment = () => {
console.log(url.length)
console.log(currImageIndex)
if (currImageIndex + 1 < url.length) {
setCurrImageIndex((oldCount) => oldCount + 1)
}
else {
setCurrImageIndex(0)
}
}
useEffect(() => {
const id = setInterval(increment, 1000);
return () => clearInterval(id);
}, []);
return (
<div className={styles.container}>
<div className={styles.header}>Preview of GIF:</div>
<img src={url[currImageIndex]} alt="GIF Preview" className={styles.image} />
<div>{currImageIndex}</div>
</div>
);
When I run this with a url of size 2, the third to last line ("{currImageIndex}") displays the 1 then 2 then 3 and so on. It does not reset ever. My console.log(url.length) prints 2 and my console.log(currImageIndex) prints 0. What I want to happen is currImageIndex should be 0, 1, 0, 1, 0, 1,.... Can anyone help me understand what I am doing wrong? Thank you!
Issue
The issue here is you've a stale enclosure of the currImageIndex state value from the initial render when the increment callback was setup in the interval timer. You're correctly using a functional state to increment the currImageIndex value, but the currImageIndex value in the increment function body is and always will be that of the initial state value, 0.
Solution
A common solution here would be to use a React ref to cache the currImageIndex state so the current value can be accessed in callbacks.
Example:
const [currImageIndex, setCurrImageIndex] = useState(0);
const currentImageIndexRef = useRef();
useEffect(() => {
// cache the current state value
currentImageIndexRef.current = currentImageIndex;
}, [currImageIndex]);
const increment = () => {
// access the current state value
if (currentImageIndexRef.current + 1 < url.length) {
setCurrImageIndex((oldCount) => oldCount + 1)
}
else {
setCurrImageIndex(0)
}
}
useEffect(() => {
const id = setInterval(increment, 1000);
return () => clearInterval(id);
}, []);
A simpler solution is to just access the url value that is closed over in component scope with the state updater callback. Use a ternary operator to check if adding 1 to the current value is still less than the url length and state + 1, otherwise reset by returning 0. Note however that if the url array is dynamic and the length changes this solution breaks for the same reason, the url value from the initial render is closed over in scope.
const increment = () => {
setCurrImageIndex((count) => count + 1 < url.length ? count + 1 : 0);
}
And IMO the simplest solution is to just increment the currImageIndex state always and take the modulus of the url length to always compute a valid, in-range index value. If the url array changes, it doesn't matter as the modulus function will always compute a valid index.
const { url } = props;
const [index, setIndex] = useState(0);
const increment = () => setIndex(i => i + 1);
useEffect(() => {
const id = setInterval(increment, 1000);
return () => clearInterval(id);
}, []);
const currImageIndex = index % url.length;
return (
<div className={styles.container}>
<div className={styles.header}>Preview of GIF:</div>
<img src={url[currImageIndex]} alt="GIF Preview" className={styles.image} />
<div>{currImageIndex}</div>
</div>
);
The reason is that the callback passed on setInterval only access the currImageIndex variable in the first render, not the new value while render so you will always get currImageIndex = 0 and pass the condition if (currImageIndex + 1 < url.length). I update your code and make it work like your expectation: https://stackblitz.com/edit/react-y6fqwt

Categories

Resources