目录
1. setInterval 失效了?
2. 正确姿势?
3. 为什么?
3.1. The Impedance Mismatch
3.2. 问题根源
3.3. Refs to the Rescue!
4. 总结
1. setInterval 失效了?
Talk is cheap. Show me the code.
import React, { useState, useEffect } from 'react';
export default function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const timerId = setInterval(() => {
console.log(new Date().toLocaleTimeString(), "=>", count);
setCount(count + 1)
}, 1000);
return () => clearInterval(timerId);
}, []);
return (
<h2>Count: {count}</h2>
);
}
运行结果:
迷之结果?
2. 正确姿势?
Talk is cheap. Show me the code.
import React, { useState, useEffect, useRef } from 'react';
function useInterval(callback: () => void, delay: number | null) {
const savedCallback = useRef<() => void>(callback);
// 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]);
}
export default function App() {
const [count, setCount] = useState(0);
useInterval(() => {
console.log(new Date().toLocaleTimeString(), "=>", count);
setCount(count + 1)
}, 1000);
return (
<h2>Count: {count}</h2>
);
}
3. 为什么?
3.1. The Impedance Mismatch
Our “impedance mismatch” is between the React programming model and the imperative setInterval API.
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.
// Describes every render
return <h1>{count}</h1>
Hooks let us apply the same declarative approach to effects:
// Describes every interval state
useInterval(() => {
setCount(count + 1);
}, isRunning ? delay : null);
We don’t set the interval, but specify whether it is set and with what delay. Our Hook makes it happen. A continuous process is described in discrete terms.
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.
3.2. 问题根源
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.
3.3. Refs to the Rescue!
The problem boils down to this:
So what if we didn’t replace the interval at all, and instead introduced a mutable savedCallback variable pointing to the latest interval callback?
Now we can see the solution:
This mutable savedCallback needs to “persist” across the re-renders. So it can’t be a regular variable. We want something more like an instance field.
useRef() gives us exactly that:
const savedCallback = useRef();
// { current: null }
useRef() returns a plain object with a mutable current property that’s shared between renders. We can save the latest interval callback into it:
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;
});
4. 总结
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.
参考:
Making setInterval Declarative with React Hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/ react-use: https://github.com/streamich/react-use