Chalarangelo/30-seconds-of-code

View on GitHub
content/snippets/react/s/use-interval.md

Summary

Maintainability
Test Coverage
---
title: React useInterval hook
shortTitle: useInterval hook
type: story
language: react
tags: [hooks,effect]
cover: clock
excerpt: Wrapping your mind around React hooks and how they interact with `setInterval()` can be difficult. Here's a guide to get you started.
listed: true
dateModified: 2024-07-04
---

Wrapping your mind around React hooks can be daunting at first, especially if you stumble into anything remotely related to **timing**, such as `setInterval()`. In order to solve such issues, you have to get used to the way hooks work, their limitations and potential workarounds.

## Intervals as a side effect

First, it should be clear that `setInterval()` is a side effect. After all, it's **not directly tied to a component's render method**. Therefore we should call it inside a `useEffect()` hook and use its `return` to call `clearInterval()` when unmounting. To avoid creating multiple intervals, we can use the hook's second argument to pass an empty dependency array (`[]`). This results in running the side effect only when the component mounts.

```jsx
React.useEffect(() => {
  let id = setInterval(callback, delay);
  return () => clearInterval(id);
}, []);
```

## The problem with closures

The closure inside `setInterval()` will only ever have access to whatever variables and values were available when it got instantiated. This means we have to be extra careful about its first argument to make sure that fresh values will be available **every time the interval runs**. The easiest solution to this issue is to create a variable that's considered **mutable** by React, using the `useRef()` hook. This will allow us to have access to new values when we need them.

```jsx
const savedCallback = React.useRef(callback);

React.useEffect(() => {
  let id = setInterval(savedCallback.current, delay);
  return () => clearInterval(id);
}, []);
```

## Updating the callback

Using the `useRef()` hook might have just shifted the problem. The value of the created ref now needs to be **refreshed** inside `setInterval()`. Luckily, this is an easy problem to solve. We could create a wrapper function that passes the function to `setInterval()` instead. This way the function passed to `setInterval()` will never change, but the value of the enclosed ref will always be up to date when it's called.

```jsx
const savedCallback = React.useRef(callback);

React.useEffect(() => {
  function tick() {
    savedCallback.current();
  }
  let id = setInterval(tick, delay);
  return () => clearInterval(id);
}, []);
```

## Making it reusable

Finally, let's extract all of this into a **custom hook** to make it reusable. We can extract `callback` as an argument for the custom hook and use it as the sole dependency of an additional `useEffect()` hook that will update the ref for the callback. Moreover, let's add a `delay` argument to the custom hook to make it more versatile.

```jsx
const useInterval = (callback, delay) => {
  const savedCallback = React.useRef();

  React.useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  React.useEffect(() => {
    const tick = () => {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
};
```

That's pretty much it. Now, to use it, you just need to call the `useInterval()` hook with a `callback` and a `delay`. Here's an example of a simple timer component that increments a counter every second:

```jsx
const Timer = props => {
  const [seconds, setSeconds] = React.useState(0);
  useInterval(() => {
    setSeconds(seconds + 1);
  }, 1000);

  return <p>{seconds}</p>;
};

ReactDOM.createRoot(document.getElementById('root')).render(
  <Timer />
);
```