How to use JavaScript scheduling methods with React hooks
At times, you may want to execute a function at a certain time later or at a specified interval. This phenomenon is called, scheduling a function call
.
JavaScript provides two methods for it,
- setInterval
- setTimeout
Using these scheduling methods with reactJs
is straightforward. However, we need to be aware of a few small gotchas to use them effectively. In this article, we will explore the usages of setInterval
and setTimeout
methods with reactJS
components.
Let us build a simple Real-time Counter
and Task Scheduler
to demonstrate the usages.
What is setInterval?
The setInterval
method allows us to run a function periodically. It starts running the function after an interval of time and then repeats continuously at that interval.
Here we have defined an interval of 1 second(1000 milliseconds) to run a function that prints some logs in the browser console.
const timerId = setInterval(() => {
console.log('Someone Scheduled me to run every second');
}, 1000);
The setInterval
function call returns a timerId
which can be used to cancel the timer by using the clearInterval
method. It will stop any further calls of setInterval.
clearInterval(timerId).
What is setTimeout?
The setTimeout
method allows us to run a function once
after the interval of the time. Here we have defined a function to log something in the browser console after 2 seconds.
const timerId = setTimeout(() => {
console.log('Will be called after 2 seconds');
}, 2000);
Like setInterval, setTimeout method call also returns a timerId
. This id can be used to stop the timer.
clearTimeout(timerId);
Real-time Counter
Let us build a real-time counter
app to understand the usage of the setInterval
method in a react application. The real-time counter has a toggle button to start and stop the counter. The counter value increments by 1 at the end of every second when the user starts the counter. The user will be able to stop the counter or resume the counter from the initial value, zero.
We will be using some of the built-in hooks from react but the same is possible using the React Class component as well.
This is how the component behaves,
Figure 1: Using setInterval and React Hooks
Step 1: Let's get started by importing React
and two in-built hooks, useState
and useEffect
.
import React, { useState, useEffect} from "react";
Step 2: We will need two state variables. First to keep track of the start-stop toggle of the real-time
button and second, for the counter
itself. Let's initialize them using the useState
hook.
The hook useState
returns a pair. First is the current state and second is an updater function. We usually take advantage of array destructuring to assign the values. The initial state value can be passed using the argument.
const [realTime, setRealTime] = useState(false);
const [counter, setCounter] = useState(0);
Step 3: The hook useEffect
is used for handling any sort of side effects like state value changes, any kind of subscriptions, network requests, etc. It takes two arguments, first a function that will be invoked on the run and, an array of the values that will run the hook.
It runs by default after every render completes. However, we can make it run whenever a particular value changes by passing it as the second parameter. We can also make it run just once by passing an empty array as the second parameter.
In this case, we are interested to run the useEffect
hook when the user toggles the real-time button(for start and stop). We want to start the interval when the realTime
state variable is true and cancel/stop the interval when the state variable value is false. Here is how the code structure may look like,
useEffect(() => {
let interval;
if (realTime) {
interval = setInterval(() => {
console.log('In setInterval');
// The logic of changing counter value to come soon.
}, 1000);
} else {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [realTime]);
We have used the setInterval
method inside the useEffect
Hook, which is the equivalent of the componentDidMount
lifecycle method in Class components. At this point, it just prints a log at the end of a 1-second interval. We are clearing the timer in two cases. First, when the value of the realTime
state variable is false, and second, the component is unmounted.
Step 4: Time to increase the counter. The most straightforward way to do that will be, call the setCounter
method and set the incremented value of the counter like this,
setCounter(counter => counter + 1);
But let us be aware of one important thing here. setInterval
method is a closure, so, when setInterval is scheduled it uses the value of the counter at that exact moment in time, which is the initial value of 0. This will make us feel, the state from the useState
hook is not getting updated inside the setInterval
method.
Have a look into this code,
useEffect(() => {
let interval;
if (realTime) {
interval = setInterval(() => {
console.log('In setInterval', counter);
}, 1000);
setCounter(100);
} else {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [realTime]);
The console.log('In setInterval', counter);
line will log 0
even when we have set the counter value to 100
. We need something special here that can keep track of the changed value of the state variable without re-rendering the component. We have another hook for it called, useRef
for this purpose.
useRef
is like a "box" or "container" that can hold a mutable value in its .current
property. We can mutate the ref
directly using counter.current = 100
. Check out this awesome article by Bhanu Teja Pachipulusu to learn about the useRef
hook in more detail.
Alright, so we need to first import it along with the other hooks.
import React, { useState, useEffect, useRef } from "react";
Then, use the useRef
hook to mutate the ref and create a sync,
const countRef = useRef(counter);
countRef.current = counter;
After this, use the countRef.current
value instead of the counter
state value inside the function passed to the setInterval
method.
useEffect(() => {
let interval;
if (realTime) {
interval = setInterval(() => {
let currCount = countRef.current;
setCounter(currCount => currCount + 1);
}, 1000);
} else {
clearInterval(interval);
}
return () => clearInterval(interval);
}, [realTime]);
Now we are guaranteed to get the updated(current) value of the counter all the time.
Step 5: Next step is to create two functions for toggling the start-stop button and resetting the counter.
const manageRealTime = () => {
setRealTime(!realTime);
}
const reset = () => {
setCounter(0);
}
Step 6: The last step is to create the rendering part of it.
<div className={style.btnGrpSpacing}>
<Button
className={style.btnSpacing}
variant={realTime? 'danger' : 'success'}
onClick={() => manageRealTime()}>
{realTime ? 'Stop Real-Time': 'Start Real-Time'}
</Button>
<Button
className={style.btnSpacing}
variant= 'info'
onClick={() => reset()}>
Reset Counter
</Button>
</div>
<div className={style.radial}>
<span>{counter}</span>
</div>
That's all. We have the real-time component working using setInterval
and react hooks(useState
, useEffect
and useRef
).
Task Scheduler
Now we will be creating another react component called, Task Scheduler
which will schedule a task of incrementing a counter by 1 after every 2 seconds. This scheduler will not do anything until the user clicks on a button to schedule again or reset the counter.
This is how the component behaves,
Figure 1: Using setTimeout and React Hooks
Just like the setInterval
method, we will use the setTimeout
method inside the useEffect
hook. We will also clear the timer when the component unmount.
useEffect(() => {
const timer = setTimeout(() => {
console.log('setTimeout called!');
}, 1000);
return () => clearTimeout(timer);
}, []);
Like setInterval, setTimeout is also a closure. Therefore, we will face a similar situation that the state variable counter
may not reflect the current value inside the setTimeout method.
useEffect(() => {
const timer = setTimeout(() => {
console.log(counter);
}, 2000);
setCounter(100);
return () => clearTimeout(timer);
}, []);
In the above case, the counter value will remain 0
even when we have set the value to 100
.
We can solve this problem similar to how we have seen it in the previous example. Use the hook useRef
.
useEffect(() => {
const timerId = schedule();
return () => clearTimeout(timerId);
}, []);
const schedule = () => {
setScheduleMessage('Scheduled in 2s...');
const timerId = setTimeout(() => {
let currCount = countRef.current;
setCounter(currCount => currCount + 1);
console.log(counter);
}, 2000);
return timerId;
}
Here we are passing the function schedule
to the setTimeout method. The schedule
function makes use of the current value from the reference(ref) and sets the counter value accordingly.
Demo and Code
You can play around with both the components from here: Demo: JavaScript scheduling with React Hooks
All the source code used in this article is part of the DemoLab GitRepo. Please feel free to clone/fork/use.
In Summary
To summarize,
setInterval
andsetTimeout
are the methods available in JavaScript to schedule function calls. Read more about it from here.- There are
clearInterval
andclearTimeout
methods to cancel the timers of the scheduler methods. - We can use these scheduler methods as similar to any other JavaScript functions in a react component.
- setInterval and setTimeout methods are a closure. Hence when scheduled, it uses the value of the state variable at the time it was scheduled. When the component re-renders a new closure is created but that doesn't change the value that was initially closed over. To fix this situation, we use the
useRef
hook to get the current value of the state variable. You can read further about this solution from this GitHub issue.
Hope you found this article helpful. You may also like,
- Understanding JavaScript Closure with example
- A Notification Timeline using React
- Understanding Dynamic imports, Lazy and Suspense using React Hooks
- Adding a Table row dynamically using React Hook
- Being Reactive - Usage of Virtual DOM and DOM Diffing
- Step by Step Guide: Blend Redux with ReactJs
You can @ me on Twitter (@tapasadhikary) with comments, or feel free to follow.
If it was useful to you, please Like/Share so that, it reaches others as well. Please hit the Subscribe button at the top of the page to get an email notification on my latest posts.