Updated state value is not reflected inside setInterval() in React - javascript

I have the following:
const [isPaused, setIsPaused] = useState(false);
const myTimer = useRef(null);
const startTimer = () => {
myTimer.current = setInterval(() => {
console.log(isPaused); // always says "false"
}, 1000);
};
Elsewhere in the code while this timer is running I'm updating the value of isPaused:
setIsPaused(true);
But this isn't reflected in the console log, it always logs false. Is there a fix to this?

The myTimer.current never changed which means isPaused is always false inside the function.
You need to make use of useEffect to update myTimer.current every time isPaused is updated.
useEffect(() => {
function startTimer() {
myTimer.current = setInterval(() => {
console.log(isPaused);
}, 1000);
};
startTimer();
return () => clearInterval(myTimer.current); // cleanup
}, [isPaused]);

You can do something like this,
const [isPaused, setIsPaused] = useState(false);
const myTimer = useRef(null);
const startTimer = () => {
myTimer.current = setInterval(() => {
console.log(isPaused); // now updates
}, 1000);
};
useEffect(() => {
startTimer();
return () => myTimer.current != null && clearInterval(myTimer.current);
}, [isPaused]);
return (
<div>
<b>isPaused: {isPaused ? "T" : "F"}</b>
<button onClick={() => setIsPaused(!isPaused)}>Toggle</button>
</div>
);
Use Others function
use useInterval from 30secondsofcode
const Timer = props => {
const [seconds, setSeconds] = React.useState(0);
useInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return <p>{seconds}</p>;
};
ReactDOM.render(<Timer />, document.getElementById('root'));
Or, use react-useInterval package
function Counter() {
let [count, setCount] = useState(0);
const increaseCount = amount => {
setCount(count + amount);
};
useInterval(increaseCount, 1000, 5);
return <h1>{count}</h1>;
}

Related

clearInterval() doesn't clear interval in React

I want to increment the number of users after each 200ms till 5000 with the below code. But it doesn't clear the interval when the number of users greater than 5000.
const Cards = () => {
const [users, setUsers] = useState(40);
useEffect(() => {
const setIntervalUsers = setInterval(() => {
setUsers((prevUsers) => prevUsers = prevUsers + 100)
}, 200);
if (users >= 5000) {
console.log('ok');
clearInterval(setIntervalUsers)
}
}, []);
return (<div>number of users {users} </div>)}
I would suggest you to return a clean up function so you don't register the interval twice in case you are in StrictMode with React 18, and also to remove it from the memory when the component gets unmounted.
Also use a ref set with useRef and a separate useEffect that would watch changes in users and clear the interval there. Like so:
import { useEffect, useRef, useState } from "react";
const Cards = () => {
const [users, setUsers] = useState(40);
const intervalRef = useRef();
useEffect(() => {
if (users >= 5000) {
console.log("ok");
clearInterval(intervalRef.current);
}
}, [users]);
useEffect(() => {
intervalRef.current = setInterval(() => {
setUsers((prevUsers) => (prevUsers = prevUsers + 100));
}, 200);
return () => clearInterval(intervalRef.current);
}, []);
return <div>number of users {users} </div>;
};
This doesnt work because:
you never call the useEffect again to check if the condition is met
the interval ref is lost
I made a working sample of your code here : https://codepen.io/aSH-uncover/pen/wvmYdNy
Addintionnaly you should clean the interval when the component is destroyed by returning the cleanInterval call in the hook that created the inteerval
const Card = ({ step }) => {
const intervals = useRef({})
const [users, setUsers] = useState(40)
useEffect(() => {
intervals.users = setInterval(() => {
setUsers((prevUsers) => prevUsers = prevUsers + step)
}, 200)
return () => clearInterval(intervals.users)
}, [])
useEffect(() => {
if (users >= 5000) {
clearInterval(intervals.users)
}
}, [users])
return (<div>number of users {users} </div>)
}
I came up with this. You can try it out. Although there are many ways suggested above
const [users, setUsers] = useState(40);
const [max_user, setMaxUser] = useState(true);
let setIntervalUsers: any;
let sprevUsers = 0;
useEffect(() => {
if (max_user) {
setIntervalUsers = setInterval(() => {
sprevUsers += 100;
if (sprevUsers >= 5000) {
setMaxUser(false);
clearInterval(setIntervalUsers);
} else {
setUsers(sprevUsers);
}
}, 200);
}
}, []);
The way how you check for your condition users >= 5000 is not working because users is not listed as a dependency in your useEffect hook. Therefore the hook only runs once but doesnt run again when users change. Because of that you only check for 40 >= 5000 once at the beginning.
An easier way to handle that is without a setInterval way.
export const Cards = () => {
const [users, setUsers] = useState(40);
useEffect(() => {
// your break condition
if (users >= 5000) return;
const increment = async () => {
// your interval
await new Promise((resolve) => setTimeout(resolve, 200));
setUsers((prevState) => prevState + 100);
}
// call your callback
increment();
// make the function run when users change.
}, [users]);
return <p>current number of users {users}</p>
}

setTimeout in useEffect hook

I am attempting to make it so that a different page from an array is displayed every 5 seconds. I currently have it working, except the page isn't always switching every 5 seconds, but sometimes 10 or 15 seconds. I believe this to be because I am not clearing the timeout correctly, any suggestions?
const pages = [
'screen1', 'screen2'
];
const [displayedPage, setDisplayedPage] = useState(pages[0]);
const [count, setCount] = useState(0);
useEffect(() => {
const timer: ReturnType<typeof setTimeout> = setTimeout(() => {
const randomNumber = pages[Math.floor(Math.random() * pages.length)];
if (randomNumber === displayedPage) {
setCount(count + 1);
clearTimeout(timer);
return timer;
}
setDisplayedPage(randomNumber);
}, 5000);
return () => clearTimeout(timer);
});
To make everlasting cycles you should use setInterval, to avoid problems with rerenders you can useRef
const pages = [ 'screen1', 'screen2' ];
const [displayedPage, setDisplayedPage] = useState(0);
const [count, setCount] = useState(0);
const timer = useRef()
useEffect(() => {
const timer.current = setInterval(() => {
const randomNumber = Math.floor(Math.random() * pages.length);
setCount(count + 1);
setDisplayedPage(current =>
randomNumber == current ?
pages[(current+1)%pages.length]
:
pages[randomNumber]
);
}, 5000);
return () => clearInterval(timer.current);
});

React: ClearInterval and Immediately Start Again

I have a component that sets off a timer which updates and makes an axios request every 30 seconds. It uses a useRef which is set to update every 30 seconds as soon as a function handleStart is fired.
const countRef = useRef(null);
const lastUpdatedRef = useRef(null);
const [lastUpdated, setLastUpdated] = useState(Date.now())
const handleStart = () => {
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1);
}, 1000);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
Now I have a useEffect that runs a calculate function every 30 seconds whenever lastUpdated is triggered as a dependency:
const firstCalculate = useRef(true);
useEffect(() => {
if (firstCalculate.current) {
firstCalculate.current = false;
return;
}
console.log("calculating");
calculateModel();
}, [lastUpdated]);
This updates the calculate function every 30 seconds (00:30, 01:00, 01:30 etc.) as per lastUpdatedRef. However, I want the timer to restart from when lastUpdated state has been modified elsewhere (e.g. if lastUpdated was modified at 00:08, the next updated will be 00:38, 01:08, 01:38 etc.). Is there a way to do this?
Basically it sounds like you just need another handler to clear and restart the 30 second interval updating the lastUpdated state.
Example:
const handleOther = () => {
clearInterval(lastUpdatedRef.current);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
}
Full example:
const calculateModel = () => console.log("calculateModel");
export default function App() {
const countRef = React.useRef(null);
const lastUpdatedRef = React.useRef(null);
const [lastUpdated, setLastUpdated] = React.useState(Date.now());
const [timer, setTimer] = React.useState(0);
const handleStart = () => {
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1);
}, 1000);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
const handleOther = () => {
clearInterval(lastUpdatedRef.current);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
const firstCalculate = React.useRef(true);
React.useEffect(() => {
if (firstCalculate.current) {
firstCalculate.current = false;
return;
}
console.log("calculating");
calculateModel();
}, [lastUpdated]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>Timer: {timer}</div>
<button type="button" onClick={handleStart}>
Start
</button>
<button type="button" onClick={handleOther}>
Other
</button>
</div>
);
}
Don't forget to clear any running intervals when the component unmounts!
React.useEffect(() => {
return () => {
clearInterval(countRef.current);
clearInterval(lastUpdatedRef.current);
};
}, []);

React: Use Interval Not Clearing

I am trying to clear the useInterval function once the joke array has 5 jokes as objects. However, I'm not sure what I'm doing wrong. For full code: https://codesandbox.io/s/asynchronous-test-mp2fq?file=/AutoComplete.js
const [joke, setJoke] = React.useState([]);
function useInterval(callback, delay) {
const savedCallback = useRef();
let id;
useEffect(() => {
savedCallback.current = callback;
if (joke.length === 5) {
console.log("5 STORED AND CLEARED INTERVAL");
return () => clearInterval(id);
}
});
useEffect(() => {
function tick() {
savedCallback.current();
}
id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
useInterval(() => {
// setJoke(joke);
axios.get("https://api.chucknorris.io/jokes/random").then((res) => {
setJoke(joke.concat(res.data.value));
console.log("JOKE: ", joke);
});
console.log("Every 5 seconds");
}, 5000);
Use a ref to store the interval id, to preserve it between re-renders. When the length is 5, call clearInterval instead of returning a clean function that would be called when the component unmounts.
In addition, make the hook agnostic of the actual stop condition by supplying a stop function, and calling it whenever the component re-renders.
function useInterval(callback, delay, stop) {
const savedCallback = useRef();
const interval = useRef();
useEffect(() => {
savedCallback.current = callback;
if (stop?.()) { // call stop to check if you need to clear the interval
clearInterval(interval.current); // call clearInterval
}
});
useEffect(() => {
function tick() {
savedCallback.current();
}
interval.current = setInterval(tick, delay); // set the current interval id to the ref
return () => clearInterval(interval.current);
}, [delay]);
}
const Example () => {
const [joke, setJoke] = React.useState([]);
useInterval(() => {
// setJoke(joke);
axios.get("https://api.chucknorris.io/jokes/random").then((res) => {
setJoke(joke.concat(res.data.value));
console.log("JOKE: ", joke);
});
console.log("Every 5 seconds");
}, 5000, () => joke.length > 4);
return (
...
);
};
Try adding in callback to your dependency array
useEffect(() => {
savedCallback.current = callback;
if (joke.length === 5) {
console.log("5 STORED AND CLEARED INTERVAL");
return () => clearInterval(id);
}
},[callback]);

Start interval after time delay and stop on button release / React

I am trying to build a simple plus/minus-control in React. When clicked on either plus or minus (triggered by onMouseDown) the value should change by a defined step and when the button is held the value should in-/decrease at a specified interval after a specified delay. When the button is released (onMouseUp), the interval should stop.
The code below runs ok on onMouseDown and hold, but when I just click on the button the interval starts anyway. I see that I need to make sure that the button is still down before the interval is started, but how do I achieve that? Thank you for any insights.
let plusTimer = useRef(null);
const increment = () => {
setMyValue(prev => prev + myStep);
setTimeout(() => {
plusTimer.current = setInterval(
() => setMyValue(prev => prev + myStep),
100
);
}, 500);
};
const intervalClear = () => {
clearInterval(plusTimer.current);
};
I think I will let the code speak for itself:
const {useCallback, useEffect, useState} = React;
const CASCADE_DELAY_MS = 1000;
const CASCADE_INTERVAL_MS = 100;
function useDelayedCascadeUpdate(intervalTime, delay, step, callback) {
const [started, setStarted] = useState(false);
const [running, setRunning] = useState(false);
const update = useCallback(() => callback((count) => count + step), [
callback,
step
]);
const handler = useCallback(() => {
update();
setStarted(true);
}, [update, setStarted]);
const reset = useCallback(() => {
setStarted(false);
setRunning(false);
}, [setStarted, setRunning]);
useEffect(() => {
if (started) {
const handler = setTimeout(() => setRunning(true), delay);
return () => {
clearTimeout(handler);
};
}
}, [started, setRunning, delay]);
useEffect(() => {
if (running) {
const handler = setInterval(update, intervalTime);
return () => {
clearInterval(handler);
};
}
}, [running, update, intervalTime]);
return [handler, reset];
}
function App() {
const [count, setCount] = useState(0);
const [incrementHandler, incrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
1,
setCount
);
const [decrementHandler, decrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
-1,
setCount
);
return (
<div>
<div>{count}</div>
<button onMouseDown={incrementHandler} onMouseUp={incrementReset}>
+
</button>
<button onMouseDown={decrementHandler} onMouseUp={decrementReset}>
-
</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

Categories