I'm coding a countdown with Expo.
I'm using functional components, so my state is handled via React's useState hook.
let [state, setState] = useState({
secondsLeft: 25,
started: false,
});
If I press a Button it does fire this function:
let onPressHandler = (): void => {
if(!state.started) {
setState({...state, started: true});
setInterval(()=> {
setState({...state, secondsLeft: state.secondsLeft - 1});
console.log(state.secondsLeft);
}, 1000);
}
}
Problem is that each 1000 ms Expo refreshes the app instead of updating the state.
Can you help me, please?
It updates the state, but it uses stale state to do so. The state variable in your setInterval callback will never change once the interval is started.
Instead, use the setter form of the state update function, so you're always working with the then-current state:
let onPressHandler = (): void => {
if(!state.started) {
setState({...state, started: true});
setInterval(()=> {
setState(currentState => {
const newState = {...currentState, secondsLeft: currentState.secondsLeft - 1};
console.log(newState.secondsLeft);
return newState;
});
}, 1000);
}
};
It's more concise without the console.log:
let onPressHandler = (): void => {
if(!state.started) {
setState({...state, started: true});
setInterval(()=> {
setState(currentState => {...currentState, secondsLeft: currentState.secondsLeft - 1});
}, 1000);
}
};
On a separate note: If you have state items that you update independently from one another, best practice is to use separate state variables for them. Also, since they're constant within your function, it's best to declare them as const. Like this:
const [secondsLeft, setSecondsLeft] = useState(25);
const [started, setStarted] = useState(false);
// ...
let onPressHandler = (): void => {
if(!started) {
setStarted(true);
setInterval(()=> {
setSecondsLeft(seconds => seconds - 1);
}, 1000);
}
};
Also, since you can't rely on setInterval to be at all precise, I suggest storing your stop time ("now" plus 25 seconds) and recalculating how many seconds are left each time:
let onPressHandler = (): void => {
const stopTime = Date.now() + (DURATION * 1000);
setStarted(true);
setSecondsLeft(DURATION);
const timer = setInterval(()=> {
const left = Math.round((stopTime - Date.now()) / 1000);
if (left <= 0) {
clearInterval(timer);
setStarted(false);
} else {
setSecondsLeft(left);
}
}, 1000);
};
Live Example (with logic for stopping):
const {useState} = React;
const Example = () => {
const DURATION = 25; // seconds
const [started, setStarted] = useState(false);
const [secondsLeft, setSecondsLeft] = useState(0);
if (started) {
return <div>Seconds left: {secondsLeft}</div>;
}
let onPressHandler = ()/*: void*/ => {
const stopTime = Date.now() + (DURATION * 1000);
setStarted(true);
setSecondsLeft(DURATION);
const timer = setInterval(()=> {
const left = Math.round((stopTime - Date.now()) / 1000);
if (left <= 0) {
clearInterval(timer);
setStarted(false);
} else {
setSecondsLeft(left);
}
}, 1000);
};
return (
<input
type="button"
onClick={onPressHandler}
value="Start"
/>
);
};
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>
Related
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>
}
From what I can tell, the timer being called in a different scope.. how can I accomplish a function to stop the timer? Going a bit crazy here, thank you for any help.
const SomeComponent = ({ isPlaying }) => {
let timer = null;
React.useEffect(() => {
if (isPlaying) {
startTimer();
}
},[isPlaying]);
const startTimer = () => {
timer = setInterval(() => {
console.log('tick');
}, 1000);
};
const stopTimer = () => {
console.log('stopping timer: ', timer); // shows null, instead of giving the timerId to stop properly
clearInterval(timer);
};
The timer variable will "reset" each time your component is re-rendered. Even if it holds your timer, a re-render will set its value to null again.
You could either move out of the component scope, or use useRef to keep the variable through re-renders:
const SomeComponent = ({ isPlaying }) => {
const timer = React.useRef(null);
React.useEffect(() => {
if (isPlaying) {
startTimer();
}
return () => clearInterval(timer.current);
}, [isPlaying]);
const startTimer = () => {
timer.current = setInterval(() => {
console.log('tick');
}, 1000);
};
const stopTimer = () => {
clearInterval(timer.current);
};
Note that I also force a clearInterval by using a return inside the useEffect. This way the component will automatically "clean up" when it unmounts. I also changed timer to be a constant.
I made a timer hook for react native which counts from from props to to props. It returns the current value of timer in seconds, start function, stop function, restart function and pause function in an object. Here is the timer code:
import { useState, useEffect, useRef } from 'react'
export default function useTimer({ from, to, intervalS, finished }) {
const [timer, setTimer] = useState(from)
const interval = useRef()
const start = () => {
if (!interval.current) {
interval.current = setInterval(() => {
setTimer(timer => timer - intervalS)
}, intervalS * 1000)
}
}
const pause = () => {
clearInterval(interval.current)
interval.current = null
}
const stop = () => {
clearInterval(interval.current)
interval.current = null
setTimer(from)
}
const restart = () => {
clearInterval(interval.current)
interval.current = null
setTimer(from)
start()
}
useEffect(() => {
return () => {
clearInterval(interval.current)
}
}, [])
useEffect(() => {
if (timer === to) {
finished()
clearInterval(interval.current)
}
}, [timer])
return {
value: timer,
start,
pause,
restart,
stop
}
}
Everything works perfectly but when i try to render (or even console.log) the timer value, it counts faster than what should be. For example the timer will pass 20 seconds for every 15 seconds in real time. I am using Expo Go app for development and my device is an android phone. Am i doing something wrong? Here is how i use timer:
export default function Countdown() {
const [finished, setFinished] = useState(false)
const timer = useTimer({ from: 60, to: 0, intervalS: 1, finished: () => setFinished(true) })
return (
<View style={{ alignItems: 'center' }}>
<Typography variant="heading">{timer.value}</Typography>
<Button title="start" onPress={timer.start} />
<Button title="pause" onPress={timer.pause} />
<Button title="restart" onPress={timer.restart} />
<Button title="stop" onPress={timer.stop} />
</View>
)
}
For anyone who might be wondering what's wrong, the problem is with expo's development mode. I don't know why but it seems timing is faster in expo's development mode. Just toggle the development mode to production mode and time will become normal
I used this approach to create a timer before:
const [intervalID, setIntervalID] = useState<any>(null)
//Initially set to 60 secs
const [testTime, setTestTime] = React.useState(60)
useEffect(() => {
setIntervalID(setInterval(() => {
updateTimer();
}, 1000));
return () => clearInterval(intervalID);
}, [])
useEffect(() => {
if (testTime <= 0) {
//Timer hit zero
return () => clearInterval(intervalID)
}
}, [testTime])
const updateTimer = () => {
setTestTime(testTime => testTime - 1)
}
function displayTime(seconds) {
const format = val => `0${Math.floor(val)}`.slice(-2)
const minutes = (seconds % 3600) / 60
return [minutes, seconds % 60].map(format).join(':')
}
const getRemainingTime = () => {
let finalTime = displayTime(testTime)
return <Text style={{ alignSelf: 'center' }}>{finalTime} minutes left</Text>
}
I had the same issue with my Expo app.
Just disable "Debug Remote JS" for it to work!
I've ResetPassword component which renders Timer component, below are their code -
ResendPassword.js
class ResetPassword extends Component{
constructor(props){
super(props);
this.state = {
resendActive: false
};
}
endHandler(){
this.setState({
resendActive: true
})
}
render(){
return (
<Timer sec={5} counter={this.state.counter} end={this.endHandler.bind(this)}/>
)
}
}
Timer.js
const Timer = (props) => {
const [sec, setSec] = useState(props.sec);
useEffect(() => {
setSec(props.sec);
const intr = setInterval(() => {
setSec((s) => {
if(s > 0)
return --s;
props.end(); // Line: causing warning
clearInterval(intr);
return s;
});
}, 1000)
return () => {
clearInterval(intr);
}
}, [props.counter])
return (
<span>{sec > 60 ? `${Math.floor(sec/60)}:${sec - Math.floor(sec/60)}`: `${sec}`} sec</span>
)
}
In Above code I'm using timer in ResetPassword and I want a function call when timer ends so I'm passing endHandler as end in Timer component but calling that function giving - Warning: Cannot update during an existing state transition (such as within 'render'), can anyone let me know what I'm doing wrong here?
Thanks In Advance
Issue
setSec is a state update function and you use the functional state update variant. This update function callback is necessarily required to be a pure function, i.e. with zero side-effects. The invocation of props.end() is a side-effect.
Solution
Split out the side-effect invocation of props.end into its own effect hook so that it is independent of the state updater function.
const Timer = (props) => {
const [sec, setSec] = useState(props.sec);
useEffect(() => {
setSec(props.sec);
const intr = setInterval(() => {
setSec((s) => {
if (s > 0) return --s;
clearInterval(intr);
return s;
});
}, 1000);
return () => {
clearInterval(intr);
};
}, [props.counter]);
useEffect(() => {
console.log(sec);
if (sec <= 0) props.end(); // <-- move invoking `end` to own effect
}, [sec]);
return (
<span>
{sec > 60
? `${Math.floor(sec / 60)}:${sec - Math.floor(sec / 60)}`
: `${sec}`}{" "}
sec
</span>
);
};
Suggestion
Create a useInterval hook
const useInterval = (callback, delay) => {
const savedCallback = useRef(null);
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
const id = setInterval(savedCallback.current, delay);
return () => clearInterval(id);
}, [delay]);
};
Update Timer to use interval hook
const Timer = ({ end, sec: secProp}) => {
const [sec, setSec] = useState(secProp);
// Only decrement sec if sec !== 0
useInterval(() => setSec((s) => s - (s ? 1 : 0)), 1000);
useEffect(() => {
!sec && end(); // sec === 0, end!
}, [sec, end]);
return (
<span>
{sec > 60
? `${Math.floor(sec / 60)}:${sec - Math.floor(sec / 60)}`
: `${sec}`}{" "}
sec
</span>
);
};
I am trying to make a pomodoro app and the count down clock will keep toggle the breaking state when the time is up, the breaking state will indicate whether you're currently working (breaking === false) or you're taking a break (breaking === true).
However, the console shows that the setBreaking are keep looping, resulting in error. I've tried to pass in an anonymous function with prevState => !prevState, error still occur. Any advice?
Here are the excerpt:
function Clock() {
const [minute, setMinute] = useState(parseInt(remainingTime/60));
const [second, setSecond] = useState(padZero(remainingTime%60));
const [breaking, setBreaking] = useState(false);
function padZero(num) {
return num.toString().padStart(2,0);
}
function countDown() {
useInterval(() => {
setRemainingTime(remainingTime-1);
}, 1000);
}
if (remainingTime === 0) {
setBreaking(!breaking) // keep looping
setCountDown(false);
setRemainingTime(breakMinute*60)
}
if (countingDown === true) {
countDown();
} else {
console.log('Timer stopped!');
}
return <h1>{minute}:{second}</h1>
};
You are not supposed to put subscriptions, timers... inside the main body. Instead, you need to put your code and start the countdown into a useEffect(..., []) hook.
By not using hooks, your code will be executed everytime you are trying to render the component, and sometimes it's kind of random...
function Clock() {
const [minute, setMinute] = useState(parseInt(remainingTime/60));
const [second, setSecond] = useState(padZero(remainingTime%60));
const [breaking, setBreaking] = useState(false);
React.useEffect(() => {
// Your code here
}, []);
return <h1>{minute}:{second}</h1>
};
Here you go with a solution using useEffect
function Clock() {
const [minute, setMinute] = useState(parseInt(remainingTime/60));
const [second, setSecond] = useState(padZero(remainingTime%60));
const [breaking, setBreaking] = useState(false);
function padZero(num) {
return num.toString().padStart(2,0);
}
function countDown() {
useInterval(() => {
setRemainingTime(remainingTime-1);
}, 1000);
}
React.useEffect(() => {
if (remainingTime === 0) {
setBreaking(!breaking);
setCountDown(false);
setRemainingTime(breakMinute*60)
}
}, [remainingTime]);
React.useEffect(() => {
if (countingDown) {
countDown();
} else {
console.log('Timer stopped!');
}
}, [countingDown]);
return <h1>{minute}:{second}</h1>
};
Using useEffect you can watch the state variable and take action based on that.