Making setInterval Declarative with React Hooks
How I learned to stop worrying and love refs.
Making setInterval Declarative with React Hooks
February 4, 2019
If you played with React Hooks for more than a few hours, you probably ran into an intriguing problem: using setInterval just doesn’t work as you’d expect.
In the words of Ryan Florence:
I’ve had a lot of people point to setInterval with hooks as some sort of egg on React’s face
Honestly, I think these people have a point. It is confusing at first.
But I’ve also come to see it not as a flaw of Hooks but as a mismatch between the React programming model and setInterval. Hooks, being closer to the React programming model than classes, make that mismatch more prominent.
There is a way to get them working together very well but it’s a bit unintuitive.
In this post, we’ll look at how to make intervals and Hooks play well together, why this solution makes sense, and which new capabilities it can give you.
Disclaimer: this post focuses on a pathological case. Even if an API simplifies a hundred use cases, the discussion will always focus on the one that got harder.
If you’re new to Hooks and don’t understand what the fuss is about, check out this introduction and the documentation instead. This post assumes that you worked with Hooks for more than an hour.
Just Show Me the Code
Without further ado, here’s a counter that increments every second:
import React, { useState, useEffect, useRef } from 'react';
function Counter() {
let [count, setCount] = useState(0);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
(Here’s a CodeSandbox demo.)
This useInterval isn’t a built-in React Hook; it’s a custom Hook that I wrote:
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
function tick() {
savedCallback.current();
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
(Here’s a CodeSandbox demo in case you missed it earlier.)
My useInterval Hook sets up an interval and clears it after unmounting. It’s a combo of setInterval and clearInterval tied to the component lifecycle.
Feel free to copy paste it in your project or put it on npm.
If you don’t care how this works, you can stop reading now! The rest of the blog post is for folks who are ready to take a deep dive into React Hooks.
Wait What?! 🤔
I know what you’re thinking:
Dan, this code doesn’t make any sense. What happened to “Just JavaScript”? Admit that React has jumped the shark with Hooks!
I thought this too but I changed my mind, and I’m going to change yours. Before explaining why this code makes sense, I want to show off what it can do.
Why useInterval() Is a Better API
To remind you, my useInterval Hook accepts a function and a delay:
useInterval(() => {
// ...
}, 1000);
This looks a lot like setInterval:
setInterval(() => {
// ...
}, 1000);
So why not just use setInterval directly?
This may not be obvious at first, but the difference between the setInterval you know and my useInterval Hook is that its arguments are “dynamic”.
I’ll illustrate this point with a concrete example.
Let’s say we want the interval delay to be adjustable:
[Counter with an input that adjusts the interval delay]
While you wouldn’t necessarily control the delay with an input, adjusting it dynamically can be useful — for example, to poll for some AJAX updates less often while the user has switched to a different tab.
So how would you do this with setInterval in a class? I ended up with this:
class Counter extends React.Component {
state = {
count: 0,
delay: 1000,
componentDidMount() {
this.interval = setInterval(this.tick, this.state.delay);
componentDidUpdate(prevProps, prevState) {
if (prevState.delay !== this.state.delay) {
clearInterval(this.interval);
this.interval = setInterval(this.tick, this.state.delay);
componentWillUnmount() {
clearInterval(this.interval);
tick = () => {
this.setState({
count: this.state.count + 1
});
handleDelayChange = (e) => {
this.setState({ delay: Number(e.target.value) });
render() {
return (
<h1>{this.state.count}</h1>
<input value={this.state.delay} onChange={this.handleDelayChange} />
</>
(Here’s a CodeSandbox demo.)
This is not too bad!
What’s the Hook version looking like?
🥁🥁🥁
function Counter() {
let [count, setCount] = useState(0);
let [delay, setDelay] = useState(1000);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, delay);
function handleDelayChange(e) {
setDelay(Number(e.target.value));
return (
<h1>{count}</h1>
<input value={delay} onChange={handleDelayChange} />
</>
(Here’s a CodeSandbox demo.)
Yeah, that’s all it takes.
Unlike the class version, there is no complexity gap for “upgrading” the useInterval Hook example to have a dynamically adjusted delay:
// Constant delay
useInterval(() => {
setCount(count + 1);
}, 1000);
// Adjustable delay
useInterval(() => {
setCount(count + 1);
}, delay);
When useInterval Hook sees a different delay, it sets up the interval again.
Instead of writing code to set and clear the interval, I can declare an interval with a particular delay — and our useInterval Hook makes it happen.
What if I want to temporarily pause my interval? I can do this with state too:
const [delay, setDelay] = useState(1000);
const [isRunning, setIsRunning] = useState(true);
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
(Here is a demo!)
This is what gets me excited about Hooks and React all over again. We can wrap the existing imperative APIs and create declarative APIs expressing our intent more closely. Just like with rendering, we can describe the process at all points in time simultaneously instead of carefully issuing commands to manipulate it.
I hope by this you’re sold on useInterval() Hook being a nicer API — at least when we’re doing it from a component.
But why is using setInterval() and clearInterval() annoying with Hooks? Let’s go back to our counter example and try to implement it manually.
First Attempt
I’ll start with a simple example that just renders the initial state:
function Counter() {
const [count, setCount] = useState(0);
return <h1>{count}</h1>;
Now I want an interval that increments it every second. It’s a side effect that needs cleanup so I’m going to useEffect() and return the cleanup function:
function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
});
return <h1>{count}</h1>;
(See the CodeSandbox demo.)
Seems easy enough? This kind of works.
However, this code has a strange behavior.
[...]