In this lesson, we'll create a Stopwatch
component that will store the current time in its state. We will then have the DOM display the current time as the state is updated.
We'll cover:
useState
hookinterval
using React's useRef
handleRunClick
function that handles flipping the running
state plus creating and clearing an interval
useEffect
hook to clear the interval
when the component unmountsInstructor: [00:00] I have the static stopwatch that shows me the amount of time that has lapsed. I have a Start and a Create button. Let's go and make this dynamic. The first thing I'm going to do is I want to make the stopwatch component render some JSX that represents the state of the component.
[00:14] The state is going to be in this lapse amount of times, we'll say lapse and we'll initialize lapse to zero. Then, we also need running state. We can switch between Start and Stop, so we'll say running. If it's running, then it should say Stop, otherwise it should say Start.
[00:33] Then, we can say running is false. Of course, that's all wired up properly. Let's go ahead and get ourselves the useState hook, and lapse is now going to be useState. We'll initialize that to zero.
[00:48] That's going to return as an array with the first item is the state and the second item is an updater of the state, so that'll be setLapse. Then, on running we'll do the same thing. We'll say useState, we initialize that to false. Then, running will be the first item of that array that's returned and sub running will be the second item of that array.
[01:08] Of course, that's all wired up properly. We can initialize it to a 100 milliseconds, or we can initialize it to running is true, and we're all set there. Next, let's go ahead and make this interactive, so we can call these setLapse and setRunning update our functions as the user interacts with our component.
[01:25] On this Start and Stop button, I'm going to say onClick handle run click, and we'll make it that a function handleRunClick. Here, we'll simply say setRrunning not running. We'll toggle the running state, so I can click on Start and it turns to Stop, I can click on Stop and turns back the Start.
[01:43] Next, let's go ahead and start the stopwatch. Here I'll say, if it's running, then we'll do something, so actually need to stop it. Otherwise, we'll get our startTime which will be date.now minus the lapse.
[02:00] Then, we'll set interval. We'll setInterval to zero which means it will call this callback as quickly as possible it can. Then, we'll call setLapse date.now minus the startTime.
[02:14] If I hit Start, the stopwatch is going to go. If I hit Stop, it does not stop. Let's go ahead and make it stop. What we get back from setInterval is an intervalId. I need to store the intervalId, so that we can clear the interval.
[02:26] In this case, we'll call clearInterval. We need to pass it the intervalId. What we're going to do is we're going to pull and useRef here. I'm going to get my intervalRef. We'll just call useRef. We'll initialize it to No.
[02:43] Then, when we call setInterval, we'll just say intervalRef.current equals setInterval. Then, when we click that stop button, we can say clearInterval intervalRef.current.
[02:56] Now, I can click Start and Stop, Start and Stop. If I hit Start and Clear that's not working. Let's wire that up. Here down in my button, I'll say onClick handle clear click. We'll make that a function called handleClearClick. It will take no parameters.
[03:15] We'll simply clearInterval, intervalRef.current. We'll set the lapse to zero and setRunning to false. Great. We had Start, Stop, Clear, and Start, and Clear. One last thing that we should do to optimize this component is, if we hit the Start button, and this setIntervals being called as frequently as it possibly can.
[03:40] Then, the component is unmounted. This setIntervals cannot continue to go. We need to make sure that we clear the interval when this component is unmounted. What we're going to do is we're going to use effect, pull that useEffect, hook in here, and we'll useEffect.
[03:56] We actually don't need to do anything when this component is mounted. We just need to do something when the component is unmounted. I'm going to pass an empty array as the second argument. Make sure that this callback function is only run one time and that is returned, cleanup function is also only run one time when the component is unmounted.
[04:17] I'm going to call clearInterval intervalRef.current, and that should get us all set to be able to Start, Stop, Clear, and have a Start, and have a unmount and clear the interval when it's unmounted.
[04:31] In review to write out the stopwatch, we first took this JSX, made it represent the state of our component. Then, we created the state of our component using useState. We wired up our buttons to these functions that took care of the logic for the stopwatch component.
[04:46] We have to keep track of the intervalId. We created an intervalRef that we use to manually set the current value to this setInterval ID. We were able to clear that interval if the user hit Stop, or if they hit Clear, or if the component is unmounted using the useEffect hook with an empty array and returning a cleanup function.
Not quite. The fact that it "works" when you try that is actually a side-effect of the fact that it's happening so quickly. Dig a little deeper and you'll see that it doesn't actually do what you think it's doing if you don't use the ref.
I'm struggling to see the difference between useRef
and useState
in the context of this example. Are they interchangeable for keeping the interval ref?
Great to know that we can use refs to something other than DOM nodes. So many info in just 5 minutes!
Krasimir, we could actually use useState
for the intervalId, and then call the updater (setIntervalId
) instead of setting intervalId.current
. The difference is that with useState
, using the updater will trigger a re-render. In our case we don't need a re-render when the interval id is set so it's better to use useRef
.
Ah right. It makes sense. Thanks for the clarification.
It's no doubt a useful tip to use useRef
instead of a variable - thanks!
Is it something related to how the browser works with clearInterval
or rather React's hook internals specifics that it doesn't work as expected with a variable rather than a value of useRef
?
It's more just about how JavaScript works. Every time the component is rendered, that function is called again. We need to have some mechanism to keep track of that value between renders and that's what refs are intended for.
Thanks!
How about a variable outside the component function?
That would work, but what if I want to render more than one in my app?
Thanks for explaining useRef in this example.
Here is the official documentation around this usage. https://reactjs.org/docs/hooks-reference.html#useref https://reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables
Thanks Gaurav for pointing out the detail documents about useRef. I can see some related phrases like
Looks like the code is no longer at sandbox...
Thanks for the great overview of new stuff! If I understood correctly, using normal variable for intervalRef instead of react reference, wouldn't change anything. But then, why did you go with react ref for it?