Mastering Intervals With React Hooks: Tips to Avoid Common Mistakes
In this article, we will dive into the world of intervals in React and how to leverage the power of hooks to handle them effectively. Whether you're a beginner or an experienced developer, understanding the nuances of intervals is crucial for building efficient and error-free applications. Let's explore some best practices to use intervals in React hooks while steering clear of common pitfalls and optimizing your projects for better performance.
Let’s look at a naive implementation of a counter that increments a number every second using setInterval
:
This code does not work.
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount(count + 1);
}, 1000);
}, []);
return (
<div className="App">
<h1>The current count is:</h1>
<h2>{count}</h2>
</div>
);
}
To understand why this code does not work, you have to understand how React deals with the state. Dan Abramov wrote a fantastic (and very long) guide to explain this.
In short, using setCount(count + 1)
we are directly referencing the count
variable from the current scope. However, in the context of setInterval
callback, the count
variable gets captured inside the CLOSURE during the Initial Render of the component. This means that the subsequent setCount
calls will always use the stale values of the count
variable.
As a result, the first time the component is rendered:
The count variable is set to 0 (initial state).
After the component is rendered and painted, React will execute the
useEffect
hook. TheuseEffect
hook will register the interval. The registered interval has access to thecount
variable (which is 0).After 1 second the callback will be invoked. It will call
setCount(0 + 1)
. This willtrigger a re-render of the component with the state
count
value as 1.
The second time the component is rendered:
The count variable is set to 1.
The updated value of
count
gets painted on the screen.The
useEffect
does not run since the empty dependency array[]
defines that the hook should only run on the first render.After 1 second the callback function gets called again but this time instead of taking count value as 1, it again takes
count
as 0. This is because of the closure associated with thesetInterval
callback function.The
setCount
method again tries to set the value of count as0 + 1
i.e.1
which is already set in the current state so React will not trigger the re-render again since the state never gets updated (All credit goes to React Fiber Diffing algorithm).
# Solving the state problem
Solution 1: Rebuild the effect on every render
Adding the count
variable in the dependency array lets our useEffect
to run on each state update.
In this case each time the setCount
is called, React will call the cleanup function (unmounts the component) before calling the useEffect again. This will call the clearInterval
function to clear the current interval and in the subsequent effect calls registers a new setInterval
which gets the updated value of the count
variable.
Here is the code:
import { useState, useEffect } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(interval)
}, [count]);
return (
<div className="App">
<h1>The current count is:</h1>
<h2>{count}</h2>
</div>
);
}
Cons: The Component is unmounting and mounting again and again.
Solution 2: Using a callback function in setCount()
Using a callback function (prevCount) => prevCount + 1)
in setCount
, React guarantees that the value passed to the state update function is the latest state value at the time of the update. React internally handles this and is able to achieve this by scheduling state updates and batching them together.
By using the functional form of setCount
, you can safely update the state based on its previous value and avoid potential bugs or incorrect state updates.
And using an empty dependency array []
ensures that the subsequent unmounting and mounting is prevented which was the bad thing in the previous solution.
Here is the code:
import { useState, useEffect } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
}, []);
return (
<div className="App">
<h1>The current count is:</h1>
<h2>{count}</h2>
</div>
);
}
Solution 3: Using the useReducer
hook
React deals with the state issue with one more efficient solution which is to use the useReducer
hook. The default behavior of the dispatch function is to access the most current state of the component. Dispatch will let you access the “future” state.
Pros:
Very flexible solution
Follows React design patterns
Here is the code:
import { useEffect, useReducer } from "react";
const reducer = (state, action) => {
switch(action.type) {
case "Increment":
return state + 1;
default:
return state;
}
}
export default function Counter() {
const [count, dispatch] = useReducer(reducer, 0);
useEffect(() => {
setInterval(() => {
dispatch({type: "Increment"});
}, 1000);
}, []);
return (
<div className="App">
<h1>The current count is:</h1>
<h2>{count}</h2>
</div>
);
}
Conclusion 🤙🏽
I hope this article will help you in gaining confidence when working with intervals and timeouts in React. Let me know if this article helped you by leaving a comment and a clap. Follow me for more informative and insightful articles in the future : ).
Subscribe to my newsletter
Read articles from Chahat Bhatia directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Chahat Bhatia
Chahat Bhatia
A passionate frontend developer who loves to code in ReactJS and other modern frontend tools.