Making SetInterval Declarative With React Hooks - Overreacted
Making SetInterval Declarative With React Hooks - Overreacted
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.
I’ve had a lot of people point to setInterval with hooks as some sort of egg on React’s
face
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.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 1/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
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.
function Counter() {
let [count, setCount] = useState(0);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
This useInterval isn’t a built-in React Hook; it’s a custom Hook that I wrote:
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 2/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
}, [callback]);
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.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 3/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
useInterval(() => {
// ...
}, 1000);
setInterval(() => {
// ...
}, 1000);
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”.
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
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 4/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
So how would you do this with setInterval in a class? I ended up with this:
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
});
}
render() {
return (
<>
<h1>{this.state.count}</h1>
<input value={this.state.delay} onChange={this.handleDelayChange} />
</>
);
}
}
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 5/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
🥁🥁🥁
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} />
</>
);
}
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);
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 6/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
}, 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:
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.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 7/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
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>;
}
React by default re-applies effects after every render. This is intentional and
helps avoid a whole class of bugs that are present in React class components.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 8/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
This is usually good because many subscription APIs can happily remove the old
and add a new listener at any time. However, setInterval isn’t one of them.
When we run clearInterval and setInterval , their timing shifts. If we re-
render and re-apply effects too often, the interval never gets a chance to fire!
We can see the bug by re-rendering our component within a smaller interval:
setInterval(() => {
// Re-renders and re-applies Counter's effects
// which in turn causes it to clearInterval()
// and setInterval() before that interval fires.
ReactDOM.render(<Counter />, rootElement);
}, 100);
Second Attempt
You might know that useEffect() lets us opt out of re-applying effects. You
can specify a dependency array as a second argument, and React will only re-
run the effect if something in that array changes:
useEffect(() => {
document.title = `You clicked ${count} times`;
}, [count]);
When we want to only run the effect on mount and cleanup on unmount, we can
pass an empty [] array of dependencies.
However, this is a common source of mistakes if you’re not very familiar with
JavaScript closures. We’re going to make this mistake right now! (We’ve also
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 9/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
In the first attempt, our problem was that re-running the effects caused our
timer to get cleared too early. We can try to fix it by never re-running them:
function Counter() {
let [count, setCount] = useState(0);
useEffect(() => {
let id = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <h1>{count}</h1>;
}
However, now our counter updates to 1 and stays there. (See the bug in action.)
What happened?!
The problem is that useEffect captures the count from the first render. It is
equal to 0 . We never re-apply the effect so the closure in setInterval always
references the count from the first render, and count + 1 is always 1 . Oops!
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 10/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
any closure. One limitation of useReducer() is that you can’t yet emit side
effects in it. (However, you could return new state — triggering some effect.)
One might say Databases are from Mars and Objects are from Venus. Databases do
not map naturally to object models. It’s a lot like trying to push the north poles of two
magnets together.
A React component may be mounted for a while and go through many different
states, but its render result describes all of them at once.
We don’t set the interval, but specify whether it is set and with what delay. Our
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 11/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
By contrast, setInterval does not describe a process in time — once you set
the interval, you can’t change anything about it except clearing it.
That’s the mismatch between the React model and the setInterval API.
Props and state of React components can change. React will re-render them and
“forget” everything about the previous render result. It becomes irrelevant.
The useEffect() Hook “forgets” the previous render too. It cleans up the last
effect and sets up the next effect. The next effect closes over fresh props and
state. This is why our first attempt worked for simple cases.
But setInterval() does not “forget”. It will forever reference the old props
and state until you replace it — which you can’t do without resetting the time.
We have callback2 from next render that closes over fresh props and state.
But we can’t replace an already existing interval without resetting the time!
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 12/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
???
PROFIT
As we can learn from the Hooks FAQ, useRef() gives us exactly that:
(You might be familiar with DOM refs in React. Hooks use the same concept for
holding any mutable values. A ref is like a “box” into which you can put anything.)
function callback() {
// Can read fresh props, state, etc.
setCount(count + 1);
}
// After every render, save the latest callback into our ref.
useEffect(() => {
savedCallback.current = callback;
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 13/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
});
And then we can read and call it from inside our interval:
useEffect(() => {
function tick() {
savedCallback.current();
}
Thanks to [] , our effect never re-executes, and the interval doesn’t get reset.
However, thanks to the savedCallback ref, we can always read the callback
that we set after the last render, and call it from the interval tick.
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 14/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
function Counter() {
const [count, setCount] = useState(0);
const savedCallback = useRef();
function callback() {
setCount(count + 1);
}
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
return <h1>{count}</h1>;
}
Extracting a Hook
Admittedly, the above code can be disorienting. It’s mind-bending to mix the
opposite paradigms. There’s also a potential to make a mess with mutable refs.
I think Hooks provide lower-level primitives than classes — but their beauty is
that they enable us to compose and create better declarative abstractions.
function Counter() {
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 15/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
I’ll copy and paste the body of my ref mechanism into a custom Hook:
function useInterval(callback) {
const savedCallback = useRef();
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
Now that the delay can change between renders, I need to declare it in the
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 16/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
useEffect(() => {
function tick() {
savedCallback.current();
}
Wait, didn’t we want to avoid resetting the interval effect, and specifically
passed [] to avoid it? Not quite. We only wanted to avoid resetting it when the
callback changes. But when the delay changes, we want to restart the timer!
function Counter() {
const [count, setCount] = useState(0);
useInterval(() => {
setCount(count + 1);
}, 1000);
return <h1>{count}</h1>;
}
useEffect(() => {
savedCallback.current = callback;
});
useEffect(() => {
function tick() {
savedCallback.current();
}
(Try it on CodeSandbox.)
It does! We can now useInterval() in any component and not think too much
about its implementation details.
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
useEffect(() => {
function tick() {
savedCallback.current();
}
That’s it. This code handles all possible transitions: a change of a delay,
pausing, or resuming an interval. The useEffect() API asks us to spend more
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 18/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
upfront effort to describe the setup and cleanup — but adding new cases is easy.
function Counter() {
const [delay, setDelay] = useState(1000);
const [count, setCount] = useState(0);
function handleReset() {
setDelay(1000);
}
return (
<>
<h1>Counter: {count}</h1>
<h4>Delay: {delay}</h4>
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 19/20
1/1/24, 10:11 PM Making setInterval Declarative with React Hooks — overreacted
<button onClick={handleReset}>
Reset delay
</button>
</>
);
}
Closing Thoughts
Hooks take some getting used to — and especially at the boundary of imperative
and declarative code. You can create powerful declarative abstractions with
them like React Spring but they can definitely get on your nerves sometimes.
This is an early time for Hooks, and there are definitely still patterns we need to
work out and compare. Don’t rush to adopt Hooks if you’re used to following
well-known “best practices”. There’s still a lot to try and discover.
I hope this post helps you understand the common pitfalls related to using APIs
like setInterval() with Hooks, the patterns that can help you overcome
them, and the sweet fruit of creating more expressive declarative APIs on top of
them.
overreacted
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ 20/20