Reputation: 95
I have created a functional component called Timer. I want to run a Timer for 3 seconds followed by a Timer for 6 seconds. However, the second timer stops as soon as it starts.
I think the problem is that the second Timer is using the state value from the first Timer. I'm not sure why it does that. I assume that the second Timer is a new instance so it shouldn't have any link with the first Timer.
Here's my code:
import React, { useEffect, useState } from 'react';
function App() {
const [timer1Up, setTimer1Up] = useState(false);
const [timer2Up, setTimer2Up] = useState(false);
if(!timer1Up) {
return <Timer duration={3} setTimeUp={setTimer1Up}/>
}
if(timer1Up && !timer2Up) {
return <Timer duration={6} setTimeUp={setTimer2Up}/>
}
if(timer1Up && timer2Up) {
return <>Out of all your time buddy!!</>
}
return <>I have no purpose</>;
}
interface TimerProps {
duration: number;
setTimeUp: any;
}
const Timer = (props: TimerProps) => {
const [timeLeft, setTimeLeft] = useState(props.duration);
useEffect(() => {
setTimeout(() => {
setTimeLeft(timeLeft - 1);
}, 1000);
if(timeLeft === 0) {
props.setTimeUp(true);
}
});
return <>{timeLeft} s</>
}
export default App;
Upvotes: 1
Views: 1301
Reputation: 84912
if(!timer1Up) {
return <Timer duration={3} setTimeUp={setTimer1Up}/>
}
if(timer1Up && !timer2Up) {
return <Timer duration={6} setTimeUp={setTimer2Up}/>
}
The main way react uses to tell whether it needs to mount a new component or reuse an existing one is by the component's type. The first time this renders, you return a <Timer>
, so react mounts a new Timer component, which then starts doing a countdown. Once the first countdown is done you render again and you also return a <Timer>
. So as far as react can tell, the only thing that changed was the props on that timer. React keeps the same component mounted, with the same state.
So there are two options 1) force it to remount, or 2) let Timer reset if its props change
To force it to remount, you will need to use keys to make it clear to react that these are different elements. That way it will unmount the old timer and mount a new one, and that new one can have its own state that it counts down
if(!timer1Up) {
return <Timer key="first" duration={3} setTimeUp={setTimer1Up}/>
}
if(timer1Up && !timer2Up) {
return <Timer key="second" duration={6} setTimeUp={setTimer2Up}/>
}
To make it work with changing props, you'll need to add logic to reset the countdown. You'll have to decide what conditions should reset the countdown, but one option is that any time the duration changes, you start over:
const Timer = (props: TimerProps) => {
const [timeLeft, setTimeLeft] = useState(props.duration);
useEffect(() => {
setTimeLeft(props.duration);
const intervalId = setInterval(() => {
setTimeLeft(prev => {
const next = prev -1;
if (next === 0) {
clearInterval(intervalId);
// Need to slightly delay calling props.setTimeUp, because setting
// state in a different component while in the middle of setting
// state here can cause an error
setTimeout(() => props.setTimeUp(true));
}
return next;
});
}, 1000);
return () => { clearInterval(intervalId); }
}, [props.duration]); // <---- dependency array to reset when the duration changes
return <>{timeLeft} s</>
}
For more information on how react decides to mount/unmount components, see this page on reconciliation.
Upvotes: 3