代码如下:https://codesandbox.io/s/nw4jym4n0
export default ({ name }: Props) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearInterval(interval);
};
});
return <h1>{counter}</h1>;
};
问题是每个setCounter
触发器都会重新渲染,因此间隔被重置并重新创建。这看起来可能很好,因为状态(计数器(一直在增加,但当与其他挂钩结合时,它可能会冻结。
正确的方法是什么?在类组件中,通过一个实例变量来保持间隔很简单。
可以给useEffect
一个空数组作为第二个参数,这样函数在初始渲染后只运行一次。由于闭包的工作方式,这将使counter
变量始终引用初始值。然后,您可以使用setCounter
的函数版本来始终获得正确的值。
示例
const { useState, useEffect } = React;
function App() {
const [counter, setCounter] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCounter(counter => counter + 1);
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
return <h1>{counter}</h1>;
};
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="root"></div>
一种更通用的方法是创建一个新的自定义钩子,该钩子将函数存储在ref
中,并且只有在delay
发生变化时才创建新的区间,就像Dan Abramov在他的伟大博客文章"strong"中所做的那样;使用React Hooks使setInterval声明"。
示例
const { useState, useEffect, useRef } = React;
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
useEffect(() => {
let id = setInterval(() => {
savedCallback.current();
}, delay);
return () => clearInterval(id);
}, [delay]);
}
function App() {
const [counter, setCounter] = useState(0);
useInterval(() => {
setCounter(counter + 1);
}, 1000);
return <h1>{counter}</h1>;
};
ReactDOM.render(
<App />,
document.getElementById('root')
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="root"></div>
@YangshunTay的另一个答案已经显示,可以使其useEffect
回调只运行一次,并且与componentDidMount
类似。在这种情况下,由于函数作用域的限制,有必要使用状态更新器函数,否则更新的counter
将无法在setInterval
回调中使用。
另一种选择是在每次计数器更新时运行useEffect
回调。在这种情况下,setInterval
应替换为setTimeout
,更新应限于counter
更新:
export default ({ name }: Props) => {
const [counter, setCounter] = useState(0);
useEffect(() => {
const timeout = setTimeout(() => {
setCounter(counter + 1);
}, 1000);
return () => {
clearTimeout(timeout);
};
}, [counter]);
return <h1>{counter}</h1>;
};
正确的方法是只运行一次效果。由于您只需要运行一次效果,因为在挂载过程中,您可以传入一个空数组作为第二个参数来实现。
但是,您需要更改setCounter
才能使用以前的counter
值。原因是传递到setInterval
的闭包中的回调在第一次渲染中只访问counter
变量,在随后的渲染中无法访问新的counter
值,因为第二次没有调用useEffect()
;CCD_ 21在CCD_。
与您熟悉的setState
一样,状态挂钩有两种形式:一种是接受更新状态的形式,另一种是传入当前状态的回调形式。您应该使用第二种形式,并在setState
回调中读取最新的状态值,以确保您在递增状态值之前拥有最新状态值。
function Counter() {
const [counter, setCounter] = React.useState(0);
React.useEffect(() => {
const timer = setInterval(() => {
setCounter(prevCount => prevCount + 1); // <-- Change this line!
}, 1000);
return () => {
clearInterval(timer);
};
}, []); // Pass in empty array to run effect only once!
return (
<div>Count: {counter}</div>
);
}
ReactDOM.render(<Counter />, document.querySelector('#app'));
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>
<div id="app"></div>