How to use JavaScript scheduling methods with React hooks

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,

setInterval_Realtime_Counter.gif 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,

setTimeout_Task_Scheduler.gif 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 and setTimeout are the methods available in JavaScript to schedule function calls. Read more about it from here.
  • There are clearInterval and clearTimeout 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,


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.

Did you find this article valuable?

Support Tapas Adhikary by becoming a sponsor. Any amount is appreciated!