setInterval pause inside useEffect - reactjs

UPDATE: OMG I'm using setTimeOut. But I still need an answer.
I made an application that accesses an array and outputs each of its elements after a certain period of time. When the array ends, execution stops.
There is a need to pause the execution. How can I do that?
const [isPaused, setIsPaused] = useState(false);
const togglePause = () => {
setIsPaused(!isPaused)
}
const soccData = data.player_positions; // cutting only IDs and positions from data
const [playerPosition, setPlayerPosition] = useState([]); // creating local state to work with IDs and positions
const getPlayerData = (arr) => { // function goes thru array of players and sets a new playerPosition on every step
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
setPlayerPosition(arr[i])
}, data.interval * (i + 1));
}
}
useEffect(() => {
getPlayerData(soccData)
return () => {
clearTimeout()
};
}, [soccData]);
return (
<div>{playerPosition}</div>
<p><button onClick={togglePause}></button></p>
)
I tried adding a condition if(!isPaused) to the function getPlayerData (and a dependency to useSeffect), but that didn't work.
Here's my code on codesandbox: https://codesandbox.io/s/youthful-paper-pvrq09
p.s. I found someone's code that allows to start/pause the execution: https://jsfiddle.net/thesyncoder/12q8r3ex/1/, but there's no ability to stop the execution when array ends.

The following works in your sandbox:
export default function App() {
const [isPaused, setIsPaused] = useState(false);
const [frame, setFrame] = useState(0);
const togglePause = () => {
setIsPaused(!isPaused);
};
const playerPosition = data.player_positions[frame];
useEffect(() => {
// do nothing if paused or the last frame is reached
if (isPaused || frame === data.player_positions.length - 1) {
return;
}
const to = setTimeout(() => {
setFrame((f) => f + 1);
}, data.interval);
return () => {
clearTimeout(to);
};
}, [frame, isPaused]);
return (
<div className="App">
<pre>{playerPosition}</pre>
<p>
<button onClick={togglePause}>{isPaused ? "play" : "pause"}</button>
</p>
</div>
);
}
As you can see, I removed the getPlayerData function. Instead, when the frame (which I called the index of the data we're currently at) or the isPaused variable changes, the effect is reran. It first (except for the very first execution) cleans up the still pending timeout (if there is one) and schedules a new one (if not paused and end is not reached) that increments the frame by one.
The main problem with your solution is the following:
const getPlayerData = (arr) => { // function goes thru array of players and sets a new playerPosition on every step
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
setPlayerPosition(arr[i])
}, data.interval * (i + 1));
}
}
This does not wait for the first timeout to occur, but instead schedules all frames in advance.
Therefore it is not pausable in a practical way (you could keep track of which timeout already ran, clear all of them when pausing and scheduling only the pending ones again once unpaused but that is not really practical)

setInterval() returns its id on execution. You can pass that id to clearInterval() to cancel it. Something like this might work:
let interval = 0;
interval = setInterval(() => {
// Your program logic
// ...
if(conditionTrueToCancelInterval) {
clearInterval(interval);
}
}, 1000)

import { ChangeEvent, useState } from 'react'
import { useInterval } from 'usehooks-ts'
export default function Component() {
// The counter
const [count, setCount] = useState<number>(0)
// Dynamic delay
const [delay, setDelay] = useState<number>(1000)
// ON/OFF
const [isPlaying, setPlaying] = useState<boolean>(false)
useInterval(
() => {
// Your custom logic here
setCount(count + 1)
},
// Delay in milliseconds or null to stop it
isPlaying ? delay : null,
)
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDelay(Number(event.target.value))
}
return (
<>
<h1>{count}</h1>
<button onClick={() => setPlaying(!isPlaying)}>
{isPlaying ? 'pause' : 'play'}
</button>
<p>
<label htmlFor="delay">Delay: </label>
<input
type="number"
name="delay"
onChange={handleChange}
value={delay}
/>
</p>
</>
)
}

Related

Switching image src with images from an array on an interval in React

This should be fairly simple, but I keep getting a weird behaviour from the result.
Basically, I have an array of images:
const images = [img1, img2, img3, img4, img5, img6];
I also have an image index:
const [imageIndex, setImageIndex] = useState(0);
Then I do a little incrementation of the index:
const switchImage = () => {
if (imageIndex === images.length - 1) {
setImageIndex(0);
} else {
setImageIndex(imageIndex + 1);
}
return imageIndex;
}
Then I call this function from a useEffect:
useEffect(() => {
setInterval(() => {
switchImage();
}, 1000);
}, []);
And finally I add the html:
<img src={images[imageIndex]} />
The result is usually it gets stuck on the second image and stops incrementing, so I thought the issue might be with the useEffect and the way the component is rendering.
You need to use the second method signature of the useState setter function which gives you the previous state value to avoid the stale closure captured value.
const root = ReactDOM.createRoot(document.getElementById('root'));
const images = ['1','2','3','4','5','6'];
const Thing =()=>{
const [imageIndex, setImageIndex] = React.useState(0);
React.useEffect(() => {
setInterval(() => {
setImageIndex(prev => (
prev === images.length - 1 ? 0 : prev + 1
));
}, 1000);
},[])
console.log(imageIndex)
return (
<div>
<h1>{images[imageIndex]}</h1>
</div>
);
}
root.render(<Thing />);
See here https://codepen.io/drGreen/pen/JjpmQrV
Also worth seeing this link which is virtually identical.
In your case the useEffect which you have created it is only being triggered once; when the component is loading - that is because you did not define when this logic should be triggered by adding dependencies to the useEffect.
Now, since the component renders once, 'switchImage'()' is only being triggered once, hence, it iterates once, display the img and stops.
Here is some good documentation on useEffect if you would like to read more about it Using the Effect Hook - React
💡Here is a slightly altered solution where we are using the debounce technique for the timer. SOLUTION💡
const root = ReactDOM.createRoot(document.getElementById('root'));
const images = ['💡','😊','😁','😍','🎯','👌'];
const DemoComponent = () =>{
const [imageIndex, setImageIndex] = React.useState(0);
//debounce set default 0.3s
const debounce = (func, timeout = 300) =>{
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => { func.apply(this, args); }, timeout);
};
}
// switch img fn.
const switchImage = () => {
setImageIndex(imageIndex === images.length - 1 ? 0 : imageIndex + 1)
return imageIndex;
}
//debounce switchImage and set timer to 1s
const switchImageDebounce = debounce(() => switchImage(),1000);
//useEffect
React.useEffect(() => {
switchImageDebounce()
}, [imageIndex]);
return (
<div>
<h1>{images[imageIndex]}</h1>
</div>
);
}
root.render();

React + Typescript: setTimeout in For Loop is not populating array consecutively, or at all

I'm building a function that makes images of random cars animate across the screen, and I want to stagger the population of the "carsLeft" array with a setTimeout..(which I will ultimately randomize the delay time).
everything works until I try to use a setTimeout. With the code below, no cars get are shown. a Console.log shows that the "carsLeft" array does not populate. When I remove the setTimeout all the cars are shown (at once of course). I have tried IIDE still no luck. Been stuck on this one for awhile, Please Help!
function Traffic() {
let carsLeft: any = [];
const generateCarLeft = () => {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
carsLeft.push(
<CarLeft key={i} className="car__left">
<img
src={carListDay[Math.floor(Math.random() * carListDay.length)]}
alt=""
/>
</CarLeft>
);
}, 3000);
}
};
generateCarLeft();
return <div className="traffic__container">{carsLeft}</div>;
}
export default Traffic;
If you want to generate elements through the loop and happen after the component is mounted is using React.useEffect hook, for example
React.useEffect(() => {
generateCarsLeft()
}, [])
and also using the carsLeft as a state will solve the problem.
If do without state and setTimeout it'll work because before first render(mount) all the data is available.
but if you use on first render list is empty and after setTimeout it'll update variable but react does not know that variable is updated you have to use the state to solve this issue.
function Traffic() {
const [carsLeft, setCarsLeft] = useState([]);
const generateCarLeft = () => {
for (let i = 0; i < 5; i++) {
setTimeout(() => {
setCarsLeft((items) => [
...items,
<CarLeft key={i} className="car__left">
<img
src={carListDay[Math.floor(Math.random() * carListDay.length)]}
alt=""
/>
</CarLeft>,
]);
}, 3000);
}
};
generateCarLeft();
return <div className="traffic__container">{carsLeft}</div>;
}
A React component is essentially just a function, so if you declare a variable inside that function it will be re-created each time you call it. If you want any value to persist between calls it has to be saved somewhere else. In React that 'somewhere' is state.
Unless React detects changes to the state it won't even re-render the component.
So as of now your component is called once, 3 seconds later all 5 new elements are added to the array at once, but the component is no re-rendered, because React is not tracking this change. I'll give you an example of how you make it work, but I'd suggest you learn more about React's concepts of state and life cycle.
function Traffic() {
[carsLeft, setCarsLeft] = React.useState([]);
React.useEffect(()=>{
if(carsLeft.length > 4) return;
setTimeout(() => {
setCarsLeft( cl => [...cl,
<CarLeft key={i} className="car__left">
<img
src={carListDay[Math.floor(Math.random() * carListDay.length)]}
alt=""/>]
);
}, 3000);
})
return <div className="traffic__container">{carsLeft}</div>;
}
This is what you are looking for
const list = ["Hello", "how", "are", "you"];
function App() {
const [myList, updateList] = useState([])
useEffect(() =>{
for (let i=0; i<list.length; i++) {
setTimeout(() => {
updateList((prvList) => ([...prvList, list[i]]));
}, 1000 * i) // * i is important
}
}, []);
return (
<div>
{
myList.map(li => (<div key={li}>{li}</div>))
}
</div>
);
}
Even if this did run, it would not run the way you want it to. Because the for loop is extremely fast, you would wait 3s to get 5 cars at once.
You need to get rid of the loop. And you need to wrap all side effects in a useEffect hook, and all persistent variables like carsLeft in a useState hook.
export default function Traffic() {
const [carsLeft, setCarsLeft] = useState<Array<ReactNode>>([]);
const timeout = useRef<number>();
useEffect(() => {
timeout.current = setTimeout(() => {
if (carsLeft.length < 5)
setCarsLeft([
...carsLeft,
<CarLeft key={carsLeft.length} className="car__left">
<img
src={carListDay[Math.floor(Math.random() * carListDay.length)]}
alt=""
/>
</CarLeft>
]);
else clearTimeout(timeout.current);
}, 3000);
return () => {
clearTimeout(timeout.current);
};
}, [carsLeft]);
return <div className="traffic__container">{carsLeft}</div>;
}
Codesandbox demo: https://codesandbox.io/s/naughty-driscoll-6k6u8?file=/src/App.tsx
I have solved all the issues and everything works perfectly. I used #Summer 's approach to remove the for loop and let the useEffect + State changes create its own endless loop. Instead of a setTimeout I used a setInterval method. An issue I had with Summer's solution was using "carsOnTheLeft.length" to generate key values. This caused multiple keys to have the same value and resulted with some issues. To fix this I created a new piece of state "carLeftIndex" to generate a unique key on every re-render. The generated objects are removed from the state array when the animation is complete thanks to React's "onAnimationEnd()' which triggers the "handleRemoveItem" function. Heres my working code, thank you for your answers!
const getRandomNumber = (min: number, max: number) => {
return Math.random() * (max - min) + min;
};
const carListDay = [car_1, car_2, car_3, car_4, car_5, car_6];
function Traffic() {
const [carsOnTheLeft, setCarsOnTheLeft] = useState<any>([]);
const timeout = useRef<any>();
const [carLeftIndex, setCarLeftIndex] = useState(0);
useEffect(() => {
const getRandomNumberToString = (min: number, max: number) => {
const result = Math.random() * (max - min) + min;
return result.toString();
};
setCarLeftIndex(carLeftIndex + 1);
const CarLeft = styled.div`
animation-duration: ${getRandomNumberToString(3, 10)}s;
`;
timeout.current = setInterval(() => {
getRandomNumberToString(2, 9);
if (carsOnTheLeft.length < 15)
setCarsOnTheLeft([
...carsOnTheLeft,
<CarLeft
key={carLeftIndex}
className="car__left"
onAnimationEnd={(e) => handleRemoveItem(e)}
>
<img
src={carListDay[Math.floor(Math.random() * carListDay.length)]}
alt=""
/>
</CarLeft>,
]);
else clearTimeout(timeout.current);
}, getRandomNumber(100, 5000));
console.log(carsOnTheLeft);
console.log(carLeftIndex);
const handleRemoveItem = (e: any) => {
setCarsOnTheLeft((carsOnTheLeft: any) =>
carsOnTheLeft.filter((key: any, i: any) => i !== 0)
);
};
return () => {
clearTimeout(timeout.current);
};
}, [carsOnTheLeft]);
return <div className="traffic__container">{carsOnTheLeft}</div>;

react pomodoro like timer with setInterval

The Idea is - I press 'start timer' and a function sets the variable 'sessionSeconds' based on whether its work 25mins or rest 5mins session (true/false). Problem is , when interval restarts, function doesn't update this 'currentSessionSeconds'. Maybe it's something with lifecycles or I should use useEffect somehow..
const TimeTracker = () => {
const [work, setWork] = useState(25);
const [rest, setRest] = useState(5);
const [remainingTime,setRemainingTime]= useState(25);
const [iswork, setIswork] = useState(true);
function startTimer() {
clearInterval(interval);
let sessionSeconds = iswork? work * 60 : rest * 60
interval = setInterval(() => {
sessionSeconds--
if (duration <= 0) {
setIswork((iswork) => !iswork)
updateTime(sessionSeconds)
startTimer();
}
}, 1000);
function updateTime(seconds){
setRemainingTime(seconds)
}
}
return (
<div>
<p>
{remainingTime}
</p>
<button onClick={startTimer}>timer</button>
</div>
);
}
I didnt include other code for converting, etc to not over clutter.
I couldn't get your code working to test it since it is missing a }.
I changed your code to include use effect and everything seems to work fine.
https://codesandbox.io/s/wizardly-saha-0dxde?file=/src/App.js
const TimeTracker = () => {
/* In your code you used use state however you didn't change
the state of these variables so I set them to constants.
You can also pass them through props.
*/
const work = 25;
const rest = 5;
const [remainingTime, setRemainingTime] = useState(work * 60);
const [isWork, setIsWork] = useState(true);
const [isTimerActive, setIsTimerActive] = useState(false);
const startTimerHandler = () => {
isWork ? setRemainingTime(work * 60) : setRemainingTime(rest * 60);
setIsTimerActive(true);
};
useEffect(() => {
if (!isTimerActive) return;
if (remainingTime === 0) {
setIsWork((prevState) => !prevState);
setIsTimerActive(false);
}
const timeOut = setTimeout(() => {
setRemainingTime((prevState) => prevState - 1);
}, 1000);
/* The return function will be called before each useEffect
after the first one and will clear previous timeout
*/
return () => {
clearTimeout(timeOut);
};
}, [remainingTime, isTimerActive]);
return (
<div>
<p>{remainingTime}</p>
<button onClick={startTimerHandler}>timer</button>
</div>
);
};

React State value not updated in Arrow functional component

React state value not updated in the console but it is updated in the view.
This is my entire code
import React, { useEffect, useState } from 'react';
const Add = (props) => {
console.log("a = ", props.a)
console.log("b = ", props.b)
const c = props.a+props.b;
return (
<div>
<p><b>{props.a} + {props.b} = <span style={{'color': 'green'}}>{c}</span></b></p>
</div>
)
}
// export default React.memo(Add);
const AddMemo = React.memo(Add);
const MemoDemo = (props) => {
const [a, setA] = useState(10)
const [b, setB] = useState(10)
const [i, setI] = useState(0);
useEffect(() => {
init()
return () => {
console.log("unmounting...")
}
}, [])
const init = () => {
console.log("init", i)
setInterval(()=>{
console.log("i = ", i)
if(i == 3){
setA(5)
setB(5)
}else{
setA(10)
setB(10)
}
setI(prevI => prevI+1)
}, 2000)
}
return (
<div>
<h2>React Memo - demo</h2>
<p>Function returns previously stored output or cached output. if inputs are same and output should same then no need to recalculation</p>
<b>I= {i}</b>
<AddMemo a={a} b={b}/>
</div>
);
}
export default MemoDemo;
Please check this image
Anyone please explain why this working like this and how to fix this
The problem is as you initialized the setInterval once so it would reference to the initial value i all the time. Meanwhile, React always reference to the latest one which always reflect the latest value on the UI while your interval is always referencing the old one. So the solution is quite simple, just kill the interval each time your i has changed so it will reference the updated value:
React.useEffect(() => {
// re-create the interval to ref the updated value
const id = init();
return () => {
// kill this after value changed
clearInterval(id);
};
// watch the `i` to create the interval
}, [i]);
const init = () => {
console.log("init", i);
// return intervalID to kill
return setInterval(() => {
// ...
});
};
In callback passed to setInterval you have a closure on the value of i=0.
For fixing it you can use a reference, log the value in the functional update or use useEffect:
// Recommended
useEffect(() => {
console.log(i);
}, [i])
const counterRef = useRef(i);
setInterval(()=> {
// or
setI(prevI => {
console.log(prevI+1);
return prevI+1;
})
// or
conosole.log(counterRef.current);
}, 2000);

how do I clearInterval on-click, with React Hooks?

I'm trying to refactor my code to react hooks, but I'm not sure if i'm doing it correctly. I tried copying and pasting my setInterval/setTimout code into hooks, but it did not work as intended. After trying different things I was able to get it to work, but I'm not sure if this is the best way to do it.
I know i can use useEffect to clear interval on un-mount, but I want to clear it before un-mounting.
Is the following good practice and if not what is a better way of clearing setInterval/setTimout before un-mounting?
Thanks,
useTimeout
import { useState, useEffect } from 'react';
let timer = null;
const useTimeout = () => {
const [count, setCount] = useState(0);
const [timerOn, setTimerOn] = useState(false);
useEffect(() => {
if (timerOn) {
console.log("timerOn ", timerOn);
timer = setInterval(() => {
setCount((prev) => prev + 1)
}, 1000);
} else {
console.log("timerOn ", timerOn);
clearInterval(timer);
setCount(0);
}
return () => {
clearInterval(timer);
}
}, [timerOn])
return [count, setCount, setTimerOn];
}
export default useTimeout;
Component
import React from 'react';
import useTimeout from './useTimeout';
const UseStateExample = () => {
const [count, setCount, setTimerOn] = useTimeout()
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={() => setCount(prev => prev + 1)}>Increment</button>
<br />
<button onClick={() => setCount(prev => prev - 1)}>Decrement</button>
<br />
<button onClick={() => setTimerOn(true)}>Set Interval</button>
<br />
<button onClick={() => setTimerOn(false)}>Stop Interval</button>
<br />
</div>
);
}
export default UseStateExample;
--- added # 2019-02-11 15:58 ---
A good pattern to use setInterval with Hooks API:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/
--- origin answer ---
Some issues:
Do not use non-constant variables in the global scope of any modules. If you use two instances of this module in one page, they’ll share those global variables.
There’s no need to clear timer in the “else” branch because if the timerOn change from true to false, the return function will be executed.
A better way in my thoughts:
import { useState, useEffect } from 'react';
export default (handler, interval) => {
const [intervalId, setIntervalId] = useState();
useEffect(() => {
const id = setInterval(handler, interval);
setIntervalId(id);
return () => clearInterval(id);
}, []);
return () => clearInterval(intervalId);
};
Running example here:
https://codesandbox.io/embed/52o442wq8l?codemirror=1
In this example, we add a couple of things...
A on/off switch for the timeout (the 'running' arg) which will completely switch it on or off
A reset function, allowing us to set the timeout back to 0 at any time:
If called while it's running, it'll keep running but return to 0.
If called while it's not running, it'll start it.
const useTimeout = (callback, delay, running = true) => {
// save id in a ref so we make sure we're always clearing the latest timeout
const timeoutId = useRef('');
// save callback as a ref so we can update the timeout callback without resetting it
const savedCallback = useRef();
useEffect(
() => {
savedCallback.current = callback;
},
[callback],
);
// clear the timeout and start a new one, updating the timeoutId ref
const reset = useCallback(
() => {
clearTimeout(timeoutId.current);
const id = setTimeout(savedCallback.current, delay);
timeoutId.current = id;
},
[delay],
);
// keep the timeout dynamic by resetting it whenever its' deps change
useEffect(
() => {
if (running && delay !== null) {
reset();
return () => clearTimeout(timeoutId.current);
}
},
[delay, running, reset],
);
return { reset };
};
So in your example above, we could use it like so...
const UseStateExample = ({delay}) => {
// count logic
const initCount = 0
const [count, setCount] = useState(initCount)
const incrementCount = () => setCount(prev => prev + 1)
const decrementCount = () => setCount(prev => prev - 1)
const resetCount = () => setCount(initCount)
// timer logic
const [timerOn, setTimerOn] = useState(false)
const {reset} = useTimeout(incrementCount, delay, timerOn)
const startTimer = () => setTimerOn(true)
const stopTimer = () => setTimerOn(false)
return (
<div>
<h2>Notes:</h2>
<p>New function are created on each render</p>
<br />
<h2>count = {count}</h2>
<button onClick={incrementCount}>Increment</button>
<br />
<button onClick={decrementCount}>Decrement</button>
<br />
<button onClick={startTimer}>Set Interval</button>
<br />
<button onClick={stopTimer}>Stop Interval</button>
<br />
<button onClick={reset}>Start Interval Again</button>
<br />
</div>
);
}
Demo of clear many timers.
You should declare and clear timer.current instead of timer.
Declare s and timer.
const [s, setS] = useState(0);
let timer = useRef<NodeJS.Timer>();
Initialize timer in useEffect(() => {}).
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
Clear timer.
useEffect(() => {
if (s == props.time) {
clearInterval(timer.current);
}
return () => {};
}, [s]);
After many attempts to make a timer work with setInterval, I decided to use setTimeOut, I hope it works for you.
const [count, setCount] = useState(60);
useEffect(() => {
if (count > 0) {
setTimeout(() => {
setCount(count - 1);
}, 1000);
}
}, [count]);

Resources