Posted on: January 22, 2023|Amrish Kushwaha

The behaviour of the setTimeout in useEffect hook

The behaviour of the setTimeout in useEffect hook

In day-to-day work, you might have come up with a requirement to perform some action after a definite delay time. I get these requirements on a regular basis in one or another form in my daily life. Sometimes, you might have a requirement of building a timer in the UI application. These tasks are common in our dev life.

If you work on the frontend part of a web application, the browser has an API to do such a job - setTimeout API. There is another API also - setInterval which does have regular intervals but in this article, we will talk about setTimeout API.

SetTimeout API

The syntax of setTimeout is

setTimeout(functionRef, delay, param1, param2, param3, ...., paramN);

Here functionRef is the function reference or callback function, the delay is the time delay and param1 … paramN are the arguments of function functionRef. The functionRef is a callback function which contains the code that runs after the delay time is over.

How internally it works:

SetTimeout API is asynchronous in nature. In the below image, you can see various parts which are involved in working with the setTimeout API.

Internal working of setTimeout

If there is a javascript program which includes setTimeout in its code and when setTimeout is invoked in the call stack, it sends that request to setTimeout API and the next line of code (after setTimeout) will run.

At setTimeout API, it will take charge of its related code and it will wait for the given delay; after the delay time is over, it will push the callback function or functionRef related code of the setTimeout to the callback queue. Since the event loop is continuously running, it will push setTimeout’s callback function to callstack when callstack becomes empty (earlier code run is passed) and then in the callback stack, setTimeout’s callback function is invoked.

This is how setTimeout works in javascript.

Working with setTimeout in React

We always come with the requirement that we need to do some task after some period of delay and for this kind of task to do in React, we normally use setTimeout in React. setTimeout’s Internal working is the same in React as it is in Javascript.

Below is an example of how we use setTimeout in React.

import React, { useEffect } from "react"
function App() {
useEffect(() => {
const timer = setTimeout(() => {
console.log("hello world")
}, 10000)
}, [])
return <h2>Example 1</h2>
}
export default App

As you can see in the above code, it is a basic example of setTimeout in React.

We have used useEffect hook. The code inside the hook will run only after rendering the DOM. Once the first rendering of UI is done then the program engine enters into useEffect code and runs it.

Since useEffect code block contains setTimeout, the request goes to setTimeout API and after 10s, it pushes the callback function to the callback queue and at the appropriate time (once the call stack is empty and the event loop will push the code to the call stack), console log will be logged the value hello world in the console.

That’s what happens.

Well, there is a glitch here with the above code.

The above code has a memory leak problem meaning that if somehow the App component is unmounted (due to page change or something) before the timer runs out, the code inside the setTimeout block will still run even after unmounting the component. This is a problem. You definitely don’t want to run a component when that component is already unmounted.

In order to solve this memory leak problem, we need to clear out the timer as soon as the component is unmounted. We can do so by returning a function which clears out the timer in useEffect code block.

Here is the improved version of the code:

import React, { useEffect } from "react"
function App() {
useEffect(() => {
const timer = setTimeout(() => {
console.log("hello world")
}, 10000)
return () => {
clearTimeout(timer)
}
}, [])
return <h2>Example 1</h2>
}
export default App

In the above code, as you can see, we are clearing out the timer inside the returned function. This improved version of the code solves the memory leak problem.

Working of setTimeout with React state

When react state is involved in the callback function of setTimeout, then the situation is now different.

Here is the code for dealing react state with setTimeout:

import React, { useEffect, useState } from "react"
function App() {
const [isDone, setDone] = useState(false)
useEffect(() => {
console.log(isDone)
setTimeout(() => {
console.log(isDone)
}, 10000)
}, [isDone])
return (
<>
<h2>Example 1</h2>
<button onClick={() => setDone(true)}>Done</button>
</>
)
}
export default App

In the above code, I have added a button which makes the isDone variable true when the user clicks on it. Since the time delay for setTimeout is 10 sec, in between if we change the state (isDone) by clicking on the button, the state will be changed, but console.log inside the setTimeout code will still print the old value false.

Solving react state issue with setTimeout:

We can solve the above problem using clearTimeout as we have done in the memory leak problem. Below is the improved version of the code which solves react state issue with setTimeout.

import React, { useEffect, useState } from "react"
function App() {
const [isDone, setDone] = useState(false)
useEffect(() => {
console.log(isDone)
const timer = setTimeout(() => {
console.log(isDone)
}, 10000)
return () => {
clearTimeout(timer)
}
}, [isDone])
return (
<>
<h2>Example 1</h2>
<button onClick={() => setDone(true)}>Done</button>
</>
)
}
export default App

One problem with this code is on every single time isDone is changed, setTimeout get reset and start again. This could be a problem if you would be doing an API call inside the setTimeout. That reset will cause one more API calls which might not necessary.

If we want to avoid restart of setTimeout on every single time state change, we can use useRef hook instead of react state.

import React, { useEffect, useRef } from "react"
function App() {
const isDone = useRef(false)
useEffect(() => {
console.log(isDone)
const timer = setTimeout(() => {
console.log(isDone)
}, 10000)
return () => {
clearTimeout(timer)
}
}, [])
const handleDone = status => {
isDone.current = status
}
return (
<>
<h2>Example 1</h2>
<button onClick={() => handleDone(true)}>Done</button>
</>
)
}
export default App

The useRef will solve the problem related to that if the latest data is not being accessed in the setTimeout code which we saw in useState hook.

I hope that you have liked the article.

Thanks for reading. Keep coding and keep solving problems.

About author:

Amrish Kushwaha

Amrish Kushwaha

I am Amrish Kushwaha. Software Engineer, Maker, Writer. I am currently focused on frontend development with curiosity of building end to end software system. Currently I am working at Rafay Systems. I love to write as well as build side projects when I am free.

Related category articles: