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.
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.