Reputation: 48566
I'm having trouble figuring out why my counter won't reset when I attempt to reset it from a control. I suspect that that I'm making some kind of novice (common and not embarrassing) mistake in how I manipulate state from within my controls.
For example, if I clock "Faster" several times and then click "Normal" counting continues at an accelerated pace: apparently the faster timer has not been cleared by the invocation of startTimer
. Only by subsequently clicking "Reset", or "Stop" followed by "Start" does the faster timer appear to clear. But I'm mystified by why this should be the case: all paths use clearInterval
in the same way.
I suspect that I'm not grasping something general about how state is manipulated in a component; or perhaps how to correctly access a timer from component state.
Why can't I get my timer to clear as expected?
WobblyCounter.tsx:
import React, { useState } from 'react'
import { View, Button, Text } from 'native-base'
import { useDispatch, useSelector } from 'react-redux'
const WobblyCounter = () => {
const [ timerID, setTimerID ] = useState(0)
const [ isRunning, updateIsRunning ] = useState(false)
const [ interval, updateInterval ] = useState(1000)
const count = useSelector((state) => state.count)
const dispatch = useDispatch()
const startTimer = (): void => {
clearInterval(timerID)
setTimerID(setInterval(() => { dispatch( {type: "INCREMENT", step: 1} ) }, interval))
updateIsRunning(true)
}
const stopTimer = (): void => {
clearInterval(timerID)
updateIsRunning(false)
}
return (
<View style={ {paddingTop:50} }>
<Button
onPress={ (): void => { dispatch( {type: "RESET"} ); startTimer() } }>
<Text>Reset</Text>
</Button>
<View style={ {flexDirection: "row"} }>
<Button small bordered dark disabled={ interval <= 250 }
onPress={ (): void => { updateInterval(Math.max(interval - 250, 250)); startTimer() } }>
<Text>Faster</Text>
</Button>
<Button small bordered dark disabled={ interval == 1000 }
onPress={ (): void => { updateInterval(1000); startTimer() } }>
<Text>Normal</Text>
</Button>
<Button small bordered dark
onPress={ (): void => { updateInterval(interval + 250); startTimer() } }>
<Text>Slower</Text>
</Button>
</View>
<Button small style={ Object.assign( {}, {backgroundColor: isRunning ? "red" : "green"} ) }
onPress={ (): void => { isRunning ? stopTimer() : startTimer() } }>
<Text>{isRunning ? "Stop" : "Start"}</Text>
</Button>
<Text>
Debug{"\n"}count = {count}{"\n"}interval = {interval}{"\n"}timerID = {timerID}
</Text>
</View>
)
}
export default WobblyCounter
Upvotes: 0
Views: 1224
Reputation: 12701
The main problem here is that the closure startTimer
is using old state value :
startTimer
is created with these values.updateInterval
is called and the interval state is changed to 750 but the component isn't rendered yet, and so startTimer
is called with the old value interval=1000.startTimer
is re-created with these values.updateInterval
is called and the interval state is changed to 1000 but the component isn't rendered yet, startTimer
is called with the old value interval=750. That is why the counter is still running fast.One way to fix this problem is to use a custom hook useInterval
proposed here by Dan Abramov, and only update the relevant state (interval, isRunning) when the buttons are clicked.
useInterval(
() => {
dispatch({ type: "INCREMENT", step: 1 });
},
isRunning ? interval : null
);
You can find the completed code here (I removed react-native)
Upvotes: 1
Reputation: 53964
You reset the timer via cleanup callback of useEffect
with an empty dep array.
Which means this side effect will run once on the component unmount:
useEffect(() => {
// startTimer will run once on component mount
startTimer();
// The cleanup callback will run once on component unmount
return stopTimer;
}, []);
But, in your case, you never unmount the component (because you dispatch actions on button clicks, that mean you always at update cycle), try put a breakpoint at stopTimer
.
Upvotes: 0