I've built a countdown counter using React hooks, but while I was comparing its accuracy with its vanilla JS counterpart (I was displaying their current timer on the document title, i.e., I was active on a third tab), I noticed that the react timer stopped after awhile , and when I opened the tab, the page refreshed and the timer reset to its initial state, while this didn't/doesn't happen in the vanilla JS version. I would like to mention that I opened maybe 15 YouTube videos, because I want the timer to be working while doing heavy duty work on my machine. How can I prevent this from happening?
Here is the code
App.js
import React, { useState } from 'react';
import convertTime from '../helper-functions/convertTime';
import useInterval from '../hooks/useInterval';
const Countdown = () => {
const [count, setCount] = useState(82.5 * 60);
const [delay, setDelay] = useState(1000);
const [isPlaying, setIsPlaying] = useState(false);
document.title = convertTime(count);
useInterval(() => setCount(count - 1), isPlaying ? delay : null);
const handlePlayClick = () => setIsPlaying(true);
const handlePauseClick = () => setIsPlaying(false);
const handleResetClick = () => {
setCount(82.5 * 60);
setDelay(1000);
setIsPlaying(false);
};
return (
<div className='counter'>
<div className='time'>{convertTime(count)}</div>
<div className='actions'>
<button onClick={handlePlayClick}>play</button>
<button onClick={handlePauseClick}>pause</button>
<button onClick={handleResetClick}>reset</button>
</div>
</div>
);
};
export default Countdown;
useInterval.js
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
export default useInterval;
Related
I am building a notification system for my app in React.js. Simply, the notifications have a timer which makes them disappear after a few seconds. This timer is animated with CSS, the width increases after each iteration of the setInterval function. Also, when hovered, the timer is stopped.
For some reason, when I generate notifications, they work correctly for the initial seconds then their timer freezes for no apparent reason. When I click the page again, they unfreeze. I've added console.logs all over the place and nothing seems to be called.
Help?
[1]: https://i.stack.imgur.com/d3Hpw.png
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'
import { useNotifications, useSetNotifications } from '../contexts/NotificationsProvider'
function Notification({note}) {
const noteRef = useRef()
const notifications = useNotifications()
const setNotifications = useSetNotifications()
const [width, setWidth] = useState(0)
const timerRef = useRef()
// sliding left animation
useEffect(() => {
// console.log("slided left")
noteRef.current.onanimationend = () => {
noteRef.current.classList.remove("slide-left")
noteRef.current.onanimationend = undefined
}
noteRef.current.classList.add("slide-left")
}, [])
// filter off notification
const removeNotification = useCallback(() => {
console.log("slided right / removed notification")
noteRef.current.onanimationend = () => {
setNotifications([...notifications.filter(listNote => listNote.id !== note.id)])
}
noteRef.current.classList.add("slide-right")
}, [note.id, notifications, setNotifications])
const handleStopTimer = useCallback(() => {
console.log("timer cleared")
clearInterval(timerRef.current)
}, [])
const handleStartTimer = useCallback(() => {
console.log("timer started")
const timerID = setInterval(() => {
setWidth(oldWidth => oldWidth + 0.5)
}, 5)
timerRef.current = timerID
}, [])
// set timer when notification first appears
useEffect(() => {
handleStartTimer()
return handleStopTimer
}, [handleStartTimer, handleStopTimer])
// remove notification when width of timer gets to the size of the notification
useLayoutEffect(() => {
if (width >= noteRef.current.clientWidth - 10) {
handleStopTimer()
removeNotification()
}
}, [width, handleStopTimer, removeNotification])
return (
<div ref={noteRef} className={`notification-item ${note.type === "SUCCESS" ? "success" : "error"}`}
onMouseEnter={handleStopTimer} onMouseLeave={handleStartTimer}>
<button onClick={removeNotification} className='closing-button'>✕</button>
<strong>{note.text}</strong>
<div className='notification-timer' style={{"width":width}}></div>
</div>
)
}
export default Notification
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).
I learning javascript, react, and i tried to count from 10 to 0, but somehow the timer only run to 9, i thought setInterval run every n time we set (n can be 1000ms, 2000ms...)
Here is the code
import "./styles.css";
import React, { useState } from "react";
export default function App() {
const [time, setTime] = useState(10);
const startCountDown = () => {
const countdown = setInterval(() => {
setTime(time - 1);
}, 1000);
if (time === 0) {
clearInterval(countdown);
}
};
return (
<div>
<button
onClick={() => {
startCountDown();
}}
>
Start countdown
</button>
<div>{time}</div>
</div>
);
}
Here is the code:
https://codesandbox.io/s/class-component-ujc9s?file=/src/App.tsx:0-506
Please explain for this, i'm so confuse, thank you
time is the value read from the state (which is the default passed passed into useState) each time the component renders.
When you click, you call setInterval with a function that closes over the time that came from the last render
Every time the component is rendered from then on, it reads a new value of time from the state.
The interval is still working with the original variable though, which is still 10.
State functions will give you the current value of the state if you pass in a callback. So use that instead of the closed over variable.
setTime(currentTime => currentTime - 1);
Just use callback in your setState function because otherwise react is working with old value of time:
import "./styles.css";
import React, { useState } from "react";
export default function App() {
const [time, setTime] = useState(10);
const startCountDown = () => {
const countdown = setInterval(() => {
setTime((prevTime)=>prevTime - 1);
}, 1000);
if (time === 0) {
clearInterval(countdown);
}
};
return (
<div>
<button
onClick={() => {
startCountDown();
}}
>
Start countdown
</button>
<div>{time}</div>
</div>
);
}
edit: You can store your interval identificator in useRef, because useRef persist through rerender and it will not cause another rerender, and then check for time in useEffect with time in dependency
import "./styles.css";
import React, { useEffect, useRef, useState } from "react";
export default function App() {
const [time, setTime] = useState(10);
const interval = useRef(0);
const startCountDown = () => {
interval.current = setInterval(() => {
setTime((prevTime) => prevTime - 1);
}, 1000);
};
useEffect(() => {
if (time === 0) {
clearInterval(interval.current);
}
}, [time]);
return (
<div>
<button
onClick={() => {
startCountDown();
}}
>
Start countdown
</button>
<div>{time}</div>
</div>
);
}
working sandbox: https://codesandbox.io/s/class-component-forked-lgiyj?file=/src/App.tsx:0-613
I'm trying out React Native by implementing a simple timer. When running the code, the counting works perfectly for the first 6 seconds, where then the app starts to act weird.
Here is the code, which you can try in this Expo Snack
import React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Button } from 'react-native';
const App = () => {
return (
<View style={styles.container}>
<Timer></Timer>
</View>
);
}
const Timer = (props) => {
const [workTime, setWorkTime] = useState({v: new Date()});
const [counter, setCounter] = useState(0);
setInterval(() => {
setCounter( counter + 1);
setWorkTime({v:new Date(counter)});
}, 1000);
return (
<View>
<Text>{workTime.v.toISOString()}</Text>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
});
export default App;
For each new component rerender, React create a new interval, which results in a memory leak and unexpected behavior.
Let us create a custom hook for interval
import { useEffect, useLayoutEffect, useRef } from 'react'
function useInterval(callback, delay) {
const savedCallback = useRef(callback)
// Remember the latest callback if it changes.
useLayoutEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
// Don't schedule if no delay is specified.
// Note: 0 is a valid value for delay.
if (!delay && delay !== 0) {
return
}
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}, [delay])
}
export default useInterval
Use it in functional component
const Timer = (props) => {
const [workTime, setWorkTime] = useState({v: new Date()});
const [counter, setCounter] = useState(0);
useInterval(()=>{
setCounter( counter + 1);
setWorkTime({v:new Date(counter)});
},1000)
return (
<View>
<Text>{workTime.v.toISOString()}</Text>
</View>
)
}
Here tested result - https://snack.expo.dev/#emmbyiringiro/58cf4b
As #AKX commented, try wrapping your interval code in a useEffect. Also, return a function from it that clears the interval (the cleanup function).
useEffect(() => {
const interval = setInterval(() => {
setCounter( counter + 1);
setWorkTime({v:new Date(counter)});
}, 1000);
return () => clearInterval(interval);
});
However, using setInterval with 1000 second delay does not yield an accurate clock, if that's what you're going for.
So the timer works. If I hard code this.state with a specific countdown number, the timer begins counting down once the page loads. I want the clock to start counting down on a button click and have a function which changes the null of the state to a randomly generated number. I am a bit new to React. I am know that useState() only sets the initial value but if I am using a click event, how do I reset useState()? I have been trying to use setCountdown(ranNum) but it crashes my app. I am sure the answer is obvious but I am just not finding it.
If I didnt provide enough code, please let me know. I didn't want to post the whole shebang.
here is my code:
import React, { useState, useEffect } from 'react';
export const Timer = ({ranNum, timerComplete}) => {
const [ countDown, setCountdown ] = useState(ranNum)
useEffect(() => {
setTimeout(() => {
countDown - 1 < 0 ? timerComplete() : setCountdown(countDown - 1)
}, 1000)
}, [countDown, timerComplete])
return ( <p >Countdown: <span>{ countDown }</span> </p> )
}
handleClick(){
let newRanNum = Math.floor(Math.random() * 20);
this.generateStateInputs(newRanNum)
let current = this.state.currentImg;
let next = ++current % images.length;
this.setState({
currentImg: next,
ranNum: newRanNum
})
}
<Timer ranNum={this.state.ranNum} timerComplete={() => this.handleComplete()} />
<Button onClick={this.handleClick} name='Generate Inputs' />
<DisplayCount name='Word Count: ' count={this.state.ranNum} />
You should store countDown in the parent component and pass it to the child component. In the parent component, you should use a variable to trigger when to start Timer.
You can try this:
import React from "react";
export default function Timer() {
const [initialTime, setInitialTime] = React.useState(0);
const [startTimer, setStartTimer] = React.useState(false);
const handleOnClick = () => {
setInitialTime(5);
setStartTimer(true);
};
React.useEffect(() => {
if (initialTime > 0) {
setTimeout(() => {
console.log("startTime, ", initialTime);
setInitialTime(initialTime - 1);
}, 1000);
}
if (initialTime === 0 && startTimer) {
console.log("done");
setStartTimer(false);
}
}, [initialTime, startTimer]);
return (
<div>
<buttononClick={handleOnClick}>
Start
</button>
<Timer initialTime={initialTime} />
</div>
);
}
const Timer = ({ initialTime }) => {
return <div>CountDown: {initialTime}</div>;
};
useState sets the initial value just like you said, but in your case I don't think you want to store the countDown in the Timer. The reason for it is that ranNum is undefined when you start the application, and gets passed down to the Timer as undefined. When Timer mounts, useEffect will be triggered with the value undefined which is something you don't want since it will trigger the setTimeout. I believe that you can store countDown in the parent of the Timer, start the timeout when the button is clicked from the parent and send the countDown value to the Timer as a prop which would make the component way easier to understand.
Here is a simple implementation using hooks and setInterval
import React, {useState, useEffect, useRef} from 'react'
import './styles.css'
const STATUS = {
STARTED: 'Started',
STOPPED: 'Stopped',
}
export default function CountdownApp() {
const [secondsRemaining, setSecondsRemaining] = useState(getRandomNum())
const [status, setStatus] = useState(STATUS.STOPPED)
const handleStart = () => {
setStatus(STATUS.STARTED)
}
const handleStop = () => {
setStatus(STATUS.STOPPED)
}
const handleRandom = () => {
setStatus(STATUS.STOPPED)
setSecondsRemaining(getRandomNum())
}
useInterval(
() => {
if (secondsRemaining > 0) {
setSecondsRemaining(secondsRemaining - 1)
} else {
setStatus(STATUS.STOPPED)
}
},
status === STATUS.STARTED ? 1000 : null,
// passing null stops the interval
)
return (
<div className="App">
<h1>React Countdown Demo</h1>
<button onClick={handleStart} type="button">
Start
</button>
<button onClick={handleStop} type="button">
Stop
</button>
<button onClick={handleRandom} type="button">
Random
</button>
<div style={{padding: 20}}>{secondsRemaining}</div>
<div>Status: {status}</div>
</div>
)
}
function getRandomNum() {
return Math.floor(Math.random() * 20)
}
// source: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
function useInterval(callback, delay) {
const savedCallback = useRef()
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback
}, [callback])
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current()
}
if (delay !== null) {
let id = setInterval(tick, delay)
return () => clearInterval(id)
}
}, [delay])
}
Here is a link to a codesandbox demo: https://codesandbox.io/s/react-countdown-demo-random-c9dm8?file=/src/App.js