在 setInterval 中使用 React 状态挂钩时状态不更新
State not updating when using React state hook within setInterval
我正在试用新的 React Hooks 并且有一个带有计数器的时钟组件,该计数器应该每秒增加一次。但是,该值不会增加超过 1。
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, 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>
原因是因为传入 setInterval
闭包的回调只访问了第一次渲染中的 time
变量,它没有访问新的 time
值在随后的渲染中,因为 useEffect()
没有被第二次调用。
time
在 setInterval
回调中的值始终为 0。
和你熟悉的setState
一样,state hooks有两种形式:一种是接受更新后的状态,另一种是传入当前状态的回调形式。你应该使用第二种形式并在 setState
回调中读取最新的状态值,以确保在递增之前拥有最新的状态值。
Bonus: Alternative Approaches
Dan Abramov, goes in-depth into the topic about using setInterval
with hooks in his blog post and provides alternative ways around this issue. Highly recommend reading it!
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(prevTime => prevTime + 1); // <-- Change this line!
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, 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>
useEffect
当提供空输入列表时,函数仅在组件安装时计算一次。
setInterval
的替代方法是在每次更新状态时使用 setTimeout
设置新间隔:
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = setTimeout(() => {
setTime(time + 1);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [time]);
setTimeout
对性能的影响微乎其微,一般可以忽略不计。除非组件对时间敏感到新设置的超时会导致不良影响,否则 setInterval
和 setTimeout
方法都是可以接受的。
另一种解决方案是使用 useReducer
,因为它将始终传递当前状态。
function Clock() {
const [time, dispatch] = React.useReducer((state = 0, action) => {
if (action.type === 'add') return state + 1
return state
});
React.useEffect(() => {
const timer = window.setInterval(() => {
dispatch({ type: 'add' });
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, 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>
当时间改变时告诉 React 重新渲染。opt out
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, [time]);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, 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>
这个解决方案对我不起作用,因为我需要获取变量并做一些事情,而不仅仅是更新它。
我找到了一个变通方法来获取带有承诺的挂钩的更新值
例如:
async function getCurrentHookValue(setHookFunction) {
return new Promise((resolve) => {
setHookFunction(prev => {
resolve(prev)
return prev;
})
})
}
有了这个,我可以像这样在 setInterval 函数中获取值
let dateFrom = await getCurrentHackValue(setSelectedDateFrom);
正如其他人指出的那样,问题是 useState
只被调用一次(如 deps = []
)以设置间隔:
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => window.clearInterval(timer);
}, []);
然后,每次 setInterval
ticks,它实际上会调用 setTime(time + 1)
,但是 time
将始终保持它在 setInterval
回调(闭包)时的初始值) 已定义。
您可以使用 useState
的 setter 的替代形式并提供回调而不是您要设置的实际值(就像 setState
一样):
setTime(prevTime => prevTime + 1);
但我鼓励您创建自己的 useInterval
钩子,这样您就可以通过使用 setInterval
declaratively, as Dan Abramov suggests here in Making setInterval Declarative with React Hooks:
来 DRY 和简化您的代码
function useInterval(callback, delay) {
const intervalRef = React.useRef();
const callbackRef = React.useRef(callback);
// Remember the latest callback:
//
// Without this, if you change the callback, when setInterval ticks again, it
// will still call your old callback.
//
// If you add `callback` to useEffect's deps, it will work fine but the
// interval will be reset.
React.useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Set up the interval:
React.useEffect(() => {
if (typeof delay === 'number') {
intervalRef.current = window.setInterval(() => callbackRef.current(), delay);
// Clear interval if the components is unmounted or the delay changes:
return () => window.clearInterval(intervalRef.current);
}
}, [delay]);
// Returns a ref to the interval ID in case you want to clear it manually:
return intervalRef;
}
const Clock = () => {
const [time, setTime] = React.useState(0);
const [isPaused, setPaused] = React.useState(false);
const intervalRef = useInterval(() => {
if (time < 10) {
setTime(time + 1);
} else {
window.clearInterval(intervalRef.current);
}
}, isPaused ? null : 1000);
return (<React.Fragment>
<button onClick={ () => setPaused(prevIsPaused => !prevIsPaused) } disabled={ time === 10 }>
{ isPaused ? 'RESUME ⏳' : 'PAUSE ' }
</button>
<p>{ time.toString().padStart(2, '0') }/10 sec.</p>
<p>setInterval { time === 10 ? 'stopped.' : 'running...' }</p>
</React.Fragment>);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
body,
button {
font-family: monospace;
}
body, p {
margin: 0;
}
p + p {
margin-top: 8px;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
button {
margin: 32px 0;
padding: 8px;
border: 2px solid black;
background: transparent;
cursor: pointer;
border-radius: 2px;
}
<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>
除了生成更简单、更清晰的代码之外,这还允许您通过简单地传递 delay = null
和 returns 间隔 ID 来自动暂停(和清除)间隔,以防您想要取消自己手动(Dan 的帖子中没有涉及)。
实际上,这也可以改进,以便它在未暂停时不会重新启动 delay
,但我想对于大多数用例来说,这已经足够了。
如果您正在寻找 setTimeout
而不是 setInterval
的类似答案,请查看:.
您还可以找到 setTimeout
和 setInterval
、useTimeout
和 useInterval
的声明版本,以及在 https://www.npmjs.com/package/@swyg/corre 中用 TypeScript 编写的一些附加挂钩。
按照下面的操作就可以了。
const [count , setCount] = useState(0);
async function increment(count,value) {
await setCount(count => count + 1);
}
//call increment function
increment(count);
useRef 可以解决这个问题,这里有一个类似的组件,每 1000 毫秒增加一次计数器
import { useState, useEffect, useRef } from "react";
export default function App() {
const initalState = 0;
const [count, setCount] = useState(initalState);
const counterRef = useRef(initalState);
useEffect(() => {
counterRef.current = count;
})
useEffect(() => {
setInterval(() => {
setCount(counterRef.current + 1);
}, 1000);
}, []);
return (
<div className="App">
<h1>The current count is:</h1>
<h2>{count}</h2>
</div>
);
}
我认为 this article 将帮助您使用间隔进行反应挂钩
我从这个博客复制了代码。归功于所有者。 https://overreacted.io/making-setinterval-declarative-with-react-hooks/
唯一的问题是我将此 React 代码改编为 React Native 代码,因此如果您是 React Native 编码人员,只需复制此代码并根据您的需要进行改编。是很容易适应的!
import React, {useState, useEffect, useRef} from "react";
import {Text} from 'react-native';
function Counter() {
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest function.
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]);
}
const [count, setCount] = useState(0);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);
return <Text>{count}</Text>;
}
export default Counter;
const [loop, setLoop] = useState(0);
useEffect(() => {
setInterval(() => setLoop(Math.random()), 5000);
}, []);
useEffect(() => {
// DO SOMETHING...
}, [loop])
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time => time + 1);// **set callback function here**
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
对于那些正在寻找极简主义解决方案的人:
- N秒后停止间隔,
- 能够在点击按钮时再次多次重置。
(无论如何我都不是 React 专家我的同事要求帮忙,我写了这篇文章并认为其他人可能会发现它有用。)
const [disabled, setDisabled] = useState(true)
const [inter, setInter] = useState(null)
const [seconds, setSeconds] = useState(0)
const startCounting = () => {
setSeconds(0)
setDisabled(true)
setInter(window.setInterval(() => {
setSeconds(seconds => seconds + 1)
}, 1000))
}
useEffect(() => {
startCounting()
}, [])
useEffect(() => {
if (seconds >= 3) {
setDisabled(false)
clearInterval(inter)
}
}, [seconds])
return (<button style = {{fontSize:'64px'}}
onClick={startCounting}
disabled = {disabled}>{seconds}</button>)
}
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((seconds) => {
if (seconds === 5) {
setSeconds(0);
return clearInterval(interval);
}
return (seconds += 1);
});
}, 1000);
}, []);
注意:这将有助于使用 useState 挂钩更新和重置计数器。 seconds 将在 5 秒后停止。因为首先更改 setSecond 值,然后在 setInterval 内以更新的秒数停止计时器。作为 useEffect 运行 一次。
某种程度上类似的问题,但是当使用 Object 并且 不更新 .
的状态值时
我对此有一些疑问,所以我希望这对某人有所帮助。
我们需要将旧对象与新对象合并
const [data, setData] = useState({key1: "val", key2: "val"});
useEffect(() => {
setData(...data, {key2: "new val", newKey: "another new"}); // --> Pass old object
}, []);
我正在试用新的 React Hooks 并且有一个带有计数器的时钟组件,该计数器应该每秒增加一次。但是,该值不会增加超过 1。
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, 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>
原因是因为传入 setInterval
闭包的回调只访问了第一次渲染中的 time
变量,它没有访问新的 time
值在随后的渲染中,因为 useEffect()
没有被第二次调用。
time
在 setInterval
回调中的值始终为 0。
和你熟悉的setState
一样,state hooks有两种形式:一种是接受更新后的状态,另一种是传入当前状态的回调形式。你应该使用第二种形式并在 setState
回调中读取最新的状态值,以确保在递增之前拥有最新的状态值。
Bonus: Alternative Approaches
Dan Abramov, goes in-depth into the topic about using
setInterval
with hooks in his blog post and provides alternative ways around this issue. Highly recommend reading it!
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(prevTime => prevTime + 1); // <-- Change this line!
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, 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>
useEffect
当提供空输入列表时,函数仅在组件安装时计算一次。
setInterval
的替代方法是在每次更新状态时使用 setTimeout
设置新间隔:
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = setTimeout(() => {
setTime(time + 1);
}, 1000);
return () => {
clearTimeout(timer);
};
}, [time]);
setTimeout
对性能的影响微乎其微,一般可以忽略不计。除非组件对时间敏感到新设置的超时会导致不良影响,否则 setInterval
和 setTimeout
方法都是可以接受的。
另一种解决方案是使用 useReducer
,因为它将始终传递当前状态。
function Clock() {
const [time, dispatch] = React.useReducer((state = 0, action) => {
if (action.type === 'add') return state + 1
return state
});
React.useEffect(() => {
const timer = window.setInterval(() => {
dispatch({ type: 'add' });
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, 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>
当时间改变时告诉 React 重新渲染。opt out
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => {
window.clearInterval(timer);
};
}, [time]);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, 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>
这个解决方案对我不起作用,因为我需要获取变量并做一些事情,而不仅仅是更新它。
我找到了一个变通方法来获取带有承诺的挂钩的更新值
例如:
async function getCurrentHookValue(setHookFunction) {
return new Promise((resolve) => {
setHookFunction(prev => {
resolve(prev)
return prev;
})
})
}
有了这个,我可以像这样在 setInterval 函数中获取值
let dateFrom = await getCurrentHackValue(setSelectedDateFrom);
正如其他人指出的那样,问题是 useState
只被调用一次(如 deps = []
)以设置间隔:
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time + 1);
}, 1000);
return () => window.clearInterval(timer);
}, []);
然后,每次 setInterval
ticks,它实际上会调用 setTime(time + 1)
,但是 time
将始终保持它在 setInterval
回调(闭包)时的初始值) 已定义。
您可以使用 useState
的 setter 的替代形式并提供回调而不是您要设置的实际值(就像 setState
一样):
setTime(prevTime => prevTime + 1);
但我鼓励您创建自己的 useInterval
钩子,这样您就可以通过使用 setInterval
declaratively, as Dan Abramov suggests here in Making setInterval Declarative with React Hooks:
function useInterval(callback, delay) {
const intervalRef = React.useRef();
const callbackRef = React.useRef(callback);
// Remember the latest callback:
//
// Without this, if you change the callback, when setInterval ticks again, it
// will still call your old callback.
//
// If you add `callback` to useEffect's deps, it will work fine but the
// interval will be reset.
React.useEffect(() => {
callbackRef.current = callback;
}, [callback]);
// Set up the interval:
React.useEffect(() => {
if (typeof delay === 'number') {
intervalRef.current = window.setInterval(() => callbackRef.current(), delay);
// Clear interval if the components is unmounted or the delay changes:
return () => window.clearInterval(intervalRef.current);
}
}, [delay]);
// Returns a ref to the interval ID in case you want to clear it manually:
return intervalRef;
}
const Clock = () => {
const [time, setTime] = React.useState(0);
const [isPaused, setPaused] = React.useState(false);
const intervalRef = useInterval(() => {
if (time < 10) {
setTime(time + 1);
} else {
window.clearInterval(intervalRef.current);
}
}, isPaused ? null : 1000);
return (<React.Fragment>
<button onClick={ () => setPaused(prevIsPaused => !prevIsPaused) } disabled={ time === 10 }>
{ isPaused ? 'RESUME ⏳' : 'PAUSE ' }
</button>
<p>{ time.toString().padStart(2, '0') }/10 sec.</p>
<p>setInterval { time === 10 ? 'stopped.' : 'running...' }</p>
</React.Fragment>);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
body,
button {
font-family: monospace;
}
body, p {
margin: 0;
}
p + p {
margin-top: 8px;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
}
button {
margin: 32px 0;
padding: 8px;
border: 2px solid black;
background: transparent;
cursor: pointer;
border-radius: 2px;
}
<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>
除了生成更简单、更清晰的代码之外,这还允许您通过简单地传递 delay = null
和 returns 间隔 ID 来自动暂停(和清除)间隔,以防您想要取消自己手动(Dan 的帖子中没有涉及)。
实际上,这也可以改进,以便它在未暂停时不会重新启动 delay
,但我想对于大多数用例来说,这已经足够了。
如果您正在寻找 setTimeout
而不是 setInterval
的类似答案,请查看:
您还可以找到 setTimeout
和 setInterval
、useTimeout
和 useInterval
的声明版本,以及在 https://www.npmjs.com/package/@swyg/corre 中用 TypeScript 编写的一些附加挂钩。
按照下面的操作就可以了。
const [count , setCount] = useState(0);
async function increment(count,value) {
await setCount(count => count + 1);
}
//call increment function
increment(count);
useRef 可以解决这个问题,这里有一个类似的组件,每 1000 毫秒增加一次计数器
import { useState, useEffect, useRef } from "react";
export default function App() {
const initalState = 0;
const [count, setCount] = useState(initalState);
const counterRef = useRef(initalState);
useEffect(() => {
counterRef.current = count;
})
useEffect(() => {
setInterval(() => {
setCount(counterRef.current + 1);
}, 1000);
}, []);
return (
<div className="App">
<h1>The current count is:</h1>
<h2>{count}</h2>
</div>
);
}
我认为 this article 将帮助您使用间隔进行反应挂钩
我从这个博客复制了代码。归功于所有者。 https://overreacted.io/making-setinterval-declarative-with-react-hooks/
唯一的问题是我将此 React 代码改编为 React Native 代码,因此如果您是 React Native 编码人员,只需复制此代码并根据您的需要进行改编。是很容易适应的!
import React, {useState, useEffect, useRef} from "react";
import {Text} from 'react-native';
function Counter() {
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest function.
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]);
}
const [count, setCount] = useState(0);
useInterval(() => {
// Your custom logic here
setCount(count + 1);
}, 1000);
return <Text>{count}</Text>;
}
export default Counter;
const [loop, setLoop] = useState(0);
useEffect(() => {
setInterval(() => setLoop(Math.random()), 5000);
}, []);
useEffect(() => {
// DO SOMETHING...
}, [loop])
function Clock() {
const [time, setTime] = React.useState(0);
React.useEffect(() => {
const timer = window.setInterval(() => {
setTime(time => time + 1);// **set callback function here**
}, 1000);
return () => {
window.clearInterval(timer);
};
}, []);
return (
<div>Seconds: {time}</div>
);
}
ReactDOM.render(<Clock />, document.querySelector('#app'));
对于那些正在寻找极简主义解决方案的人:
- N秒后停止间隔,
- 能够在点击按钮时再次多次重置。
(无论如何我都不是 React 专家我的同事要求帮忙,我写了这篇文章并认为其他人可能会发现它有用。)
const [disabled, setDisabled] = useState(true)
const [inter, setInter] = useState(null)
const [seconds, setSeconds] = useState(0)
const startCounting = () => {
setSeconds(0)
setDisabled(true)
setInter(window.setInterval(() => {
setSeconds(seconds => seconds + 1)
}, 1000))
}
useEffect(() => {
startCounting()
}, [])
useEffect(() => {
if (seconds >= 3) {
setDisabled(false)
clearInterval(inter)
}
}, [seconds])
return (<button style = {{fontSize:'64px'}}
onClick={startCounting}
disabled = {disabled}>{seconds}</button>)
}
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds((seconds) => {
if (seconds === 5) {
setSeconds(0);
return clearInterval(interval);
}
return (seconds += 1);
});
}, 1000);
}, []);
注意:这将有助于使用 useState 挂钩更新和重置计数器。 seconds 将在 5 秒后停止。因为首先更改 setSecond 值,然后在 setInterval 内以更新的秒数停止计时器。作为 useEffect 运行 一次。
某种程度上类似的问题,但是当使用 Object 并且 不更新 .
的状态值时我对此有一些疑问,所以我希望这对某人有所帮助。 我们需要将旧对象与新对象合并
const [data, setData] = useState({key1: "val", key2: "val"});
useEffect(() => {
setData(...data, {key2: "new val", newKey: "another new"}); // --> Pass old object
}, []);