how to set/reset 'useInterval' upon button press or tab change - javascript

I am building Pomodoro timer and I am having trouble with useIntervel hook. I want to both be able to reset the useInterval so that the timer should stop when reset button is pressed or I navigate to another tab (I am using bottom tab navigation) and restart when the start button is pressed or I re-navigate to the timer tab. I can stop the useInterval hook thanks to the article:
[1]: React hooks useInterval reset after button click
However, once stopped I can't reactivate it.
I tried the following but it didn't work as it throws the "invalid hook call" error:
function useInterval (callback, delay){
// rest of the code
}
useEffect(()=> {
const interval = useInterval(callback, delay)
}, [start])
In the above link "Jacki" mentions that
Reset actually stop but doesn't start new interval but I figured out how to do so thanks to your answer and now everything works fine. Thank you!
However he hasn't shared a solution which is what I want.

I would implement a custom hook that manages an internal start and stop state.
Here is a minimal example. The delay prop is the input for the setInterval function. The callback prop is a function that is called on each interval tick.
export function useInterval(callback, delay) {
const savedCallback = useRef();
const [start, setStart] = useState(false)
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
if (start) {
const tick = () => {
savedCallback.current();
}
if (delay !== null) {
const id = setInterval(tick, delay);
return () => clearInterval(id);
}
}
}, [delay, start]);
const startTimer = React.useCallback((shouldStart) => {
setStart(shouldStart)
}, []);
return startTimer;
}
You can use it as follows.
export default function App() {
const [timer, setTimer] = useState(0);
const startTimer = useInterval(() => setTimer(prev => prev + 1), 100)
return (
<View style={{margin:50}}>
<Pressable onPress={() => startTimer(true)}> <Text>Start</Text></Pressable>
<Pressable onPress={() => startTimer(false)}> <Text>Stop</Text></Pressable>
<Pressable onPress={() => setTimer(0)}><Text>Reset</Text></Pressable>
<Text>{timer}</Text>
</View>
);
}
Here is a little snack. You can start, stop and reset the timer via the buttons.

Related

Canceling a timeout in useEffect() if user scrolls with react hooks

Basically the same question as How to cancel a javascript function if the user scrolls the page but using react hooks.
I wrote react code that scrolls down to the end of the page after 3 seconds.
const scrollToEnd = () => { /* implementation omitted */ }
useEffect(() => {
const id = setTimeout(() => scrollToEnd(), 3000)
return () => clearTimeout(id)
}, [])
I want modify this code so that if the user manually scrolls the page before this timeout, the timeout is cleared.
I was thinking of a solution like:
const [hasScrolled, setHasScrolled] = useState(false);
const scrollToEnd = () => { /* implementation omitted */ }
useEffect(() => {
const setHasScrolledCallback = () => setHasScrolled(true)
window.addEventListener("scroll", setHasScrolledCallback);
return () => window.removeEventListener("scroll", setHasScrolledCallback);
}, []);
useEffect(() => {
const scrollCallback = () => { if (hasScrolled) scrollToEnd() }
const id = setTimeout(scrollCallback, 3000)
return () => clearTimeout(id)
}, [])
This works, but I don't think this is the correct way to approach this problem, because the scroll event is fired multiple times, even after the timeout occurs. Also the scrollCallback isn't really canceled, it runs anyway even if it does nothing.

clearInterval not working in React Application using functional component

I wanted to build a timer application in React using functional component and below are the requirements.
The component will display a number initialized to 0 know as counter.
The component will display a Start button below the counter number.
On clicking the Start button the counter will start running. This means the counter number will start incrementing by 1 for every one second.
When the counter is running(incrementing), the Start button will become the Pause button.
On clicking the Pause button, the counter will preserve its value (number) but stops running(incrementing).
The component will also display a Reset button.
On clicking the Reset button, the counter will go to its initial value(which is 0 in our case) and stops running(incrementing).
Below is the code that I have implemented, but clearInterval doesn't seems to be working, Also how do i implement Reset Button?
Code:
import React, { useState, useEffect } from "react";
export default function Counter() {
const [counter, setCounter] = useState(0);
const [flag, setFlag] = useState(false);
const [isClicked, setClicked] = useState(false);
var myInterval;
function incrementCounter() {
setClicked(!isClicked);
if (flag) {
myInterval = setInterval(
() => setCounter((counter) => counter + 1),
1000
);
setFlag(false);
} else {
console.log("sasdsad");
clearInterval(myInterval);
}
}
function resetCounter() {
clearInterval(myInterval);
setCounter(0);
}
useEffect(() => {
setFlag(true);
}, []);
return (
<div>
<p>{counter}</p>
<button onClick={incrementCounter}>
{isClicked ? "Pause" : "Start"}
</button>
<button onClick={resetCounter}>Reset</button>
</div>
);
}
Codesandbox link:
CodeSandbox
I did a slightly different version that use an extra useEffect that runs on isRunning (changed name from flag) change:
import React, { useState, useEffect, useRef } from "react";
export default function Counter() {
const [counter, setCounter] = useState(0);
// Change initial value to `false` if you don't want
// to have timer running on load
// Changed `flag` name to more significant name
const [isRunning, setIsRunning] = useState(false);
// You don't need 2 variable for this
//const [isClicked, setClicked] = useState(false);
// Using `useRef` to store a reference to the interval
const myInterval = useRef();
useEffect(() => {
// You had this line to start timer on load
// but you can just set the initial state to `true`
//setFlag(true);
// Clear time on component dismount
return () => clearInterval(myInterval.current);
}, []);
// useEffect that start/stop interval on flag change
useEffect(() => {
if (isRunning) {
myInterval.current = setInterval(
() => setCounter((counter) => counter + 1),
1000
);
} else {
clearInterval(myInterval.current);
myInterval.current = null;
}
}, [isRunning]);
// Now on click you only change the flag
function toggleTimer() {
setIsRunning((isRunning) => !isRunning);
}
function resetCounter() {
clearInterval(myInterval.current);
myInterval.current = null;
setCounter(0);
setIsRunning(false);
}
return (
<div>
<p>{counter}</p>
<button onClick={toggleTimer}>{isRunning ? "Pause" : "Start"}</button>
<button onClick={resetCounter}>Reset</button>
</div>
);
}
Demo: https://codesandbox.io/s/dank-night-wwxqz3?file=/src/Counter.js
As a little extra i've made a version that uses a custom hook useTimer. In this way the component code is way cleaner:
https://codesandbox.io/s/agitated-curie-nkjf62?file=/src/Counter.js
Use useRef to make the interval as a ref. Then use resetCounter() to clean the interval ref.
const intervalRef = useRef(null)
const incrementCounter = () => {
intervalRef.current = setInterval(() => {
setCounter(prevState => prevState + 1)
}, 1000);
};
const resetCounter = () => {
clearInterval(intervalRef.current);
intervalRef.current = null;
};
Between each rendering your variable myInterval value doesn't survive. That's why you need to use the [useRef][1] hook that save the reference of this variable across each rendering.
Besides, you don't need an flag function, as you have all information with the myClicked variable
Here is a modification of your code with those modifications. Don't hesitate if you have any question.
import React, { useState, useEffect, useRef } from "react";
export default function Counter() {
const [counter, setCounter] = useState(0);
const [isStarted, setIsStarted] = useState(false);
const myInterval = useRef();
function start() {
setIsStarted(true);
myInterval.current = setInterval(() => setCounter((counter) => counter + 1), 100);
100;
}
function pause() {
setIsStarted(false);
clearInterval(myInterval.current);
}
function resetCounter() {
clearInterval(myInterval.current);
setCounter(0);
}
return (
<div>
<p>{counter}</p>
{!isStarted ?
<button onClick={start}>
Start
</button>
:
<button onClick={pause}>
Pause
</button>
}
<button onClick={resetCounter}>Reset</button>
</div>
);
}
\\\
[1]: https://reactjs.org/docs/hooks-reference.html#useref
I'll just leave this here for anyone having the same problem.
in my case, the issue was node setInterval was used instead of window.setInterval.
this is a problem since this returns a type of Node.Timer which is an object instead of number (setInterval ID) for the clearInterval() to work as it needs an argument type of number. so to fix this,
React.useEffect(() => {
let timeoutId;
timeoutId = window.setInterval(callback, 100);
return = () => {
if(timeoutId) clearInterval(timeoutId)
}
}, [])
or in class components use componentWillMount()
You have to store myInterval in state. After that when button is clicked and flag is false, you can clear interval (myInterval in state).

Timer/Counter for react component - value remains 0 after increasing it with setInterval()

export default function Timer() {
const [timer, setTimer] = useState(0)
const checkTimer = () => {
console.log(timer);
}
useEffect(() => {
const timer = setInterval(() => {
setTimer(prevCount => prevCount + 1);
}, 1000);
startProgram(); //This starts some other functions
return () => {
checkTimer();
clearInterval(timer);
}
}, [])
}
Above is a simplified version of my code and the main issue - I am trying to increase the timer state by setting an interval in useEffect() (only once). However, in checkTimer() the value is always 0, even though the console statement execute every second. I am new to reactjs and would appreciate some help as this is already taking me too many hours to fix.
checkTimer is showing you the initial value of timer state because of stale closure. That means at the time when useEffect was executed, (i.e. once at component mount due to [] as dependency), it registered a cleanup function which created a closure around checkTimer function (and everything, state or props values, it uses). And when this closure was created the value of timer state was 0. And it will always remain that.
There are few options to fix it.
One quick solution would be to use useRef:
const timer = useRef(0);
const checkTimer = () => {
console.log(timer.current);
};
useEffect(() => {
const id = setInterval(() => {
timer.current++;
}, 1000);
return () => {
checkTimer();
clearInterval(id);
};
}, []);
Check this related post to see more code examples to fix this.
Edit:
And, if you want to show the timer at UI as well, we need to use state as we know that "ref" data won't update at UI. So, the option 2 is to use "updater" form of setTimer to read the latest state data in checkTimer function:
const [timer, setTimer] = useState(0);
const checkTimer = () => {
let prevTimer = 0;
setTimer((prev) => {
prevTimer = prev; // HERE
return prev; // Returning same value won't change state data and won't causes a re-render
});
console.log(prevTimer);
};
useEffect(() => {
const id = setInterval(() => {
setTimer((prev) => prev + 1);
}, 1000);
return () => {
checkTimer();
clearInterval(id);
};
}, []);

What happens if state is updated multiple times?

When I run the code below, I expected the counter to be set to zero and start again like 1..2..3 and so on.
But when I click reset (on 5) it goes like 0..6..1..7..2.. and so on.
Why does this happen? What am I missing here? Thanks.
const App = (props) => {
const [counter, setCounter] = useState(0)
setTimeout(
() => setCounter(counter + 1),
1000
)
return (
<div>
<h1>{counter}</h1>
<button onClick={()=>setCounter(0)}>Reset</button>
</div>
)
}
It's best to run this kind of effect in a useEffect hook. That way, you can run the effect whenever counter is updated and provide a mechanism to cancel an existing timeout.
When the timeout hits 1 second, the counter stateful variable will be incremented. Since counter is specified in the useEffect dependency array, the effect will run again, queuing up another timeout.
We return a cleanup function from our userEffect hook. This is important because, if our counter is changed by some other mechanism (e.g., the Reset button), we'll want to cancel the in-progress timeout to start over!
import React, { useState, useEffect } from "react";
const App = (props) => {
const [counter, setCounter] = useState(0)
useEffect(() => {
const timeout = setTimeout(
() => setCounter(counter + 1),
1000
)
return () => {
clearTimeout(timeout);
}
}, [counter])
return (
<div>
<h1>{counter}</h1>
<button onClick={()=>setCounter(0)}>Reset</button>
</div>
)
}

Confusion about the inputs array of useEffect(React Hooks API)

Some React Hooks API like useEffect, useMemo, useCallback have a second parameter: an array of inputs:
useEffect(didUpdate, inputs);
As the official document said:
#see Conditionally firing an effect
That way an effect is always recreated if one of its inputs changes.
every value referenced inside the effect function should also appear in the inputs array.
So we can see, the inputs array takes two responsibilities.
In most situations, they are working properly. But sometimes they conflict.
For example, I have a little counting program, it does two things:
Click button and the count plus 1.
Send the count to server every 5 seconds.
Codesandbox:
https://codesandbox.io/s/k0m1mq9v
Or see the code here:
import { useState, useEffect, useCallback } from 'react';
function xhr(count) {
console.log(`Sending "${count}" to my server.`);
// TODO send count to my server by XMLHttpRequest
}
function add1(n) {
return n + 1;
}
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
// Handle click to increase count by 1
const handleClick = useCallback(
() => setCount(add1),
[],
);
// Send count to server every 5 seconds
useEffect(() => {
const intervalId = setInterval(() => xhr(count), 5000);
return () => clearInterval(intervalId);
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>
Click me
</button>
</div>
);
}
export default Example;
When I run this code, I'll always send count = 0 to my server, because I haven't passed the count to useEffect.
But if I pass count to useEffect, my setInterval will be cleared and the whole callback will be recreated each time when I click the button.
I think maybe there's another paradigm to achieve my goal which I haven't think of. If not, that is a conflict of the inputs array.
Reply from React:
React discussion Github
A better solution may be implemented, but not now.
But life will continue, so a workaround pattern like this may help:
const [count, setCount] = useState(0);
const countRef = useRef(count);
useEffect(() => {
countRef.current = count
}, [count]);
useRef() can solve your problem. I think this is an elegant solution: code in sandbox
function App() {
const [count, setCount] = useState(0);
// ***** Initialize countRef.current with count
const countRef = useRef(count);
const handleClick = useCallback(() => setCount(add1), []);
// ***** Set countRef.current to current count
// after comment https://github.com/facebook/react/issues/14543#issuecomment-452996829
useEffect(() => (countRef.current = count));
useEffect(() => {
// ***** countRef.current is xhr function argument
const intervalId = setInterval(() => xhr(countRef.current), 5000);
return () => clearInterval(intervalId);
}, []);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={handleClick}>Click me</button>
</div>
);
}
EDIT
After comment: https://github.com/facebook/react/issues/14543#issuecomment-452996829

Categories