Mastering Intervals With React Hooks: Tips to Avoid Common Mistakes

Chahat BhatiaChahat Bhatia
5 min read

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:

  1. The count variable is set to 0 (initial state).

  2. After the component is rendered and painted, React will execute the useEffecthook. The useEffect hook will register the interval. The registered interval has access to the count variable (which is 0).

  3. After 1 second the callback will be invoked. It will call setCount(0 + 1). This will

    trigger a re-render of the component with the state count value as 1.

The second time the component is rendered:

  1. The count variable is set to 1.

  2. The updated value of count gets painted on the screen.

  3. The useEffect does not run since the empty dependency array [] defines that the hook should only run on the first render.

  4. 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 the setInterval callback function.

  5. The setCount method again tries to set the value of count as 0 + 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 : ).

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