How to increment a state variable every second in React - javascript

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

Related

How to set state with a setInterval() when a button is clicked?

I'm trying to update the count of activeIndex within the setInterval when the checkbox is ticked. handleNext() and handlePrevious() are working fine when buttons are clicked but when the checkbox is checked the value of activeIndex is not getting updated in handleNext() but it's getting updated on the screen, so there is no condition check for activeIndex and it goes beyond 3.
import { useState } from "react";
import "./styles.css";
export default function App() {
const [slideTimer, setSlideTimer] = useState(null);
const [activeIndex, setActiveIndex] = useState(0);
const slideDuration = 1000;
const handleNext = () => {
if ((activeIndex) >= 3) {
setActiveIndex(0);
} else {
setActiveIndex(prev => prev + 1);
}
};
const handlePrev = () => {
if (activeIndex <= 0) {
setActiveIndex(3);
} else {
setActiveIndex((prev) => prev - 1);
}
};
const toggleSlider = () => {
if (slideTimer) {
clearInterval(slideTimer);
setSlideTimer(null);
return;
}
setSlideTimer(setInterval(handleNext, slideDuration));
};
return (
<div className="App">
<h1>{activeIndex}</h1>
<button onClick={() => handleNext()}>Next</button>
<button onClick={() => handlePrev()}>Previous</button>
<input onClick={() => toggleSlider()} type="checkbox" />
</div>
);
}
I have tried putting the code for toggleSlider inside useEffect() but that too is not working.
Your problem is that when handleNext gets defined (which occurs on every rerender), it only knows about the variables/state in its surrounding scope at the time that it's defined. As a result, when you queue an interval with:
setInterval(handleNext, slideDuration)
You will be executing the handleNext function that only knows about the component's state at that time. When your component eventually rerenders and sets a new value for activeIndex, your interval will still be exeucting the "old" handleNext function defined in the previous render that doesn't know about the newly updated state. One option to resolve this issue is to make the hanldeNext function not rely on state obtained from its outer scope, but instead, use the state setter function to get the current value:
const handleNext = () => {
setActiveIndex(currIndex => (currIndex + 1) % 4);
};
Above I've used % to cycle the index back to 0, but you very well can also use your if-statement, where you return 0 if currIndex >= 3, or return currIndex + 1 in your else.
I would also recommend that you remove the slideTimer. This state value isn't being used to describe your UI (you can see that as you're not using slideTimer within your returned JSX). In this case, you're better off using a ref to store your interval id:
const slideTimerRef = useRef(0); // instead of the `slideTimer` state
...
clearInterval(slideTimerRef.current);
slideTimerRef.current = null;
...
slideTimerRef.current = setInterval(handleNext, slideDuration);

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

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

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>

React useState not working with conditional operator

This is my codes in React.
const [click, setClick] = useState(false);
const [like, setLike] = useState(dataObj.likedCount); // like === 4
const onClick = () => {
console.log(like);
console.log(click);
return setClick(!click) ? setLike(like - 1) : setLike(like + 1);
};
return (
<Like onClick={onClick}>{like}</Like>
)
And this is console page.
my trouble is that 'like number' is only increasing, not decreasing.
Even if boolean is changing, true to false.
I can't find anything wrong on my codes.... :((
You're using the return value of setClick. The setter function from useState doesn't have a documented return value. Experimentation suggests it always returns undefined. So you can't rely on the return value from setClick.
If your goal is to use the value of !click, do so directly:
setClick(!click);
if (!click) {
setLike(like - 1); // Or you may want + 1 here
} else {
setLike(like + 1); // Or you may want - 1 here
}
or
setClick(!click);
setLike(like + (click ? 1 : -1)); // Again, you may want to swap 1 and -1,
// depending on whether you want to use the
// old value or the new one
Some notice points:
Use state instead of setClick(!click) for conditional operator
do not return in onClick(), use props value (checked) to make it fully controlled
const [click, setClick] = useState(false);
const [like, setLike] = useState(dataObj.likedCount);
const onClick = () => {
setLike(like + (click ? -1 : 1)); // status before click been used here
setClick(!click);
};
return (
<Like onClick={onClick} value={click}>{like}</Like>
)

When boolean turns false, check 5 seconds if it will turn back to true, else do some action

I have in NodeJS a variable that updates every second. I want to monitor it to see if it turns below a certain threshold (e.g. 1000).
If it does go below the threshold, it should wait 5 seconds and monitor if the variable goes back above again. If not, it should return a function. If it does go above, it can stop the times and start monitoring again.
Can't get any code to work.
Not sure if the code below is even in the right direction..!
var waitedSoFar = 0;
var imageDisplayed = CheckIfImageIsDisplayed(); //this function is where you check the condition
while(waitedSoFar < 5000)
{
imageDisplayed = CheckIfImageIsDisplayed();
if(imageDisplayed)
{
//success here
break;
}
waitedSoFar += 100;
Thread.Sleep(100);
}
if(!imageDisplayed)
{
//failed, do something here about that.
}
You could have a function that observe and wrap this variable that change so often. Something as simple as
function observeValue(initialValue) {
let value = initialValue
let clearEffects = []
let subscribers = []
return {
value: () => value,
change(val) {
clearEffects.forEach(oldEffect => oldEffect && oldEffect())
value = val
clearEffects = subscribers.map(subscriber => subscriber(value))
},
subscribe(listener) {
subscribers.push(listener)
}
}
}
is generic enough. The function returns three methods:
one to get the current value
one to change the value
on to subscribe on every value change
I would suggest to leverage the last one to monitor and trigger any side effects.
You can use it like this:
const monitor = observeValue(true)
monitor.subscribe((value) => {
if (value !== false) {
return
}
const timeout = setTimeout(() => {
console.log('value was false for five seconds')
}, 5000)
return () => {
clearTimeout(timeout)
}
})
and somewhere else you can change the value with
monitor.change(false)

Categories

Resources