在 useEffect 中使用旧状态反应间隔
React interval using old state inside of useEffect
我 运行 遇到了从 useEffect 内部设置间隔计时器的情况。我可以在 useEffect 中访问组件变量和状态,并且间隔计时器按预期运行。但是,计时器回调无权访问组件变量/状态。通常,我希望这是“this”的问题。但是,我不相信“这”是这里的情况。没有双关语的意思。我在下面包含了一个简单的例子:
import React, { useEffect, useState } from 'react';
const App = () => {
const [count, setCount] = useState(0);
const [intervalSet, setIntervalSet] = useState(false);
useEffect(() => {
if (!intervalSet) {
setInterval(() => {
console.log(`count=${count}`);
setCount(count + 1);
}, 1000);
setIntervalSet(true);
}
}, [count, intervalSet]);
return <div></div>;
};
export default App;
控制台每秒只输出count=0。我知道有一种方法可以将函数传递给更新当前状态的 setCount 并且在这个简单的示例中有效。但是,这不是我要表达的意思。实际代码比我在这里展示的要复杂得多。我的真实代码查看由异步 thunk 操作管理的当前状态对象。另外,我知道我没有包含组件卸载时的清理功能。对于这个简单的示例,我不需要它。
第一次 运行 useEffect
时, intervalSet
变量设置为 true
并且您的区间函数是使用当前值 (0) 创建的。
在 useEffect
的后续 运行 中,由于 intervalSet
检查,它不会重新创建间隔,而是继续 运行 现有间隔,其中计数是原始值 (0).
你把事情弄得比需要的更复杂了。
useState
set 函数可以接受一个函数,该函数传递状态的当前值和 returns 新值,即 setCount(currentValue => newValue);
卸载组件时应始终清除间隔,否则当它尝试设置状态并且状态不再存在时,您会遇到问题。
import React, { useEffect, useState } from 'react';
const App = () => {
// State to hold count.
const [count, setCount] = useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
export default App;
您可以 运行 代码并查看下面的代码。
const App = () => {
// State to hold count.
const [count, setCount] = React.useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
React.useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
ReactDOM.render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
问题是间隔只创建一次并一直指向相同的状态值。我建议 - 移动触发间隔以分隔 useEffect
,因此它在组件安装时启动。将间隔存储在变量中,以便您能够重新启动它或清除它。最后 - 每次卸载时清除它。
const App = () => {
const [count, setCount] = React.useState(0);
const [intervalSet, setIntervalSet] = React.useState(false);
React.useEffect(() => {
setIntervalSet(true);
}, []);
React.useEffect(() => {
const interval = intervalSet ? setInterval(() => {
setCount((c) => {
console.log(c);
return c + 1;
});
}, 1000) : null;
return () => clearInterval(interval);
}, [intervalSet]);
return null;
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
如果您需要更复杂的实现,如前所述 ,您或许应该尝试使用 ref。例如,这是我在项目中使用的自定义间隔挂钩。你可以看到有一个效果,如果它改变了就会更新回调。
这确保您始终拥有最新的状态值,并且您不需要使用像 setCount(count => count + 1)
.
这样的自定义更新函数语法
const useInterval = (callback, delay) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
if (delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay])
}
// Usage
const App = () => {
useInterval(() => {
// do something every second
}, 1000)
return (...)
}
这是一个您可以使用的非常灵活的选项。但是,此挂钩 假定 您希望在组件安装时开始间隔。您的代码示例使我相信您希望它根据 intervalSet
布尔值的状态变化开始。您可以更新自定义间隔挂钩,或在您的组件中实现它。
在您的示例中看起来像这样:
const useInterval = (callback, delay, initialStart = true) => {
const [start, setStart] = React.useState(initialStart)
const savedCallback = React.useRef()
React.useEffect(() => {
savedCallback.current = callback
}, [callback])
React.useEffect(() => {
if (start && delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay, start])
// this function ensures our state is read-only
const startInterval = () => {
setStart(true)
}
return [start, startInterval]
}
const App = () => {
const [countOne, setCountOne] = React.useState(0);
const [countTwo, setCountTwo] = React.useState(0);
const incrementCountOne = () => {
setCountOne(countOne + 1)
}
const incrementCountTwo = () => {
setCountTwo(countTwo + 1)
}
// Starts on component mount by default
useInterval(incrementCountOne, 1000)
// Starts when you call `startIntervalTwo(true)`
const [intervalTwoStarted, startIntervalTwo] = useInterval(incrementCountTwo, 1000, false)
return (
<div>
<p>started: {countOne}</p>
<p>{intervalTwoStarted ? 'started' : <button onClick={startIntervalTwo}>start</button>}: {countTwo}</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
我 运行 遇到了从 useEffect 内部设置间隔计时器的情况。我可以在 useEffect 中访问组件变量和状态,并且间隔计时器按预期运行。但是,计时器回调无权访问组件变量/状态。通常,我希望这是“this”的问题。但是,我不相信“这”是这里的情况。没有双关语的意思。我在下面包含了一个简单的例子:
import React, { useEffect, useState } from 'react';
const App = () => {
const [count, setCount] = useState(0);
const [intervalSet, setIntervalSet] = useState(false);
useEffect(() => {
if (!intervalSet) {
setInterval(() => {
console.log(`count=${count}`);
setCount(count + 1);
}, 1000);
setIntervalSet(true);
}
}, [count, intervalSet]);
return <div></div>;
};
export default App;
控制台每秒只输出count=0。我知道有一种方法可以将函数传递给更新当前状态的 setCount 并且在这个简单的示例中有效。但是,这不是我要表达的意思。实际代码比我在这里展示的要复杂得多。我的真实代码查看由异步 thunk 操作管理的当前状态对象。另外,我知道我没有包含组件卸载时的清理功能。对于这个简单的示例,我不需要它。
第一次 运行 useEffect
时, intervalSet
变量设置为 true
并且您的区间函数是使用当前值 (0) 创建的。
在 useEffect
的后续 运行 中,由于 intervalSet
检查,它不会重新创建间隔,而是继续 运行 现有间隔,其中计数是原始值 (0).
你把事情弄得比需要的更复杂了。
useState
set 函数可以接受一个函数,该函数传递状态的当前值和 returns 新值,即 setCount(currentValue => newValue);
卸载组件时应始终清除间隔,否则当它尝试设置状态并且状态不再存在时,您会遇到问题。
import React, { useEffect, useState } from 'react';
const App = () => {
// State to hold count.
const [count, setCount] = useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
export default App;
您可以 运行 代码并查看下面的代码。
const App = () => {
// State to hold count.
const [count, setCount] = React.useState(0);
// Use effect to create and clean up the interval
// (should only run once with current dependencies)
React.useEffect(() => {
// Create interval get the interval ID so it can be cleared later.
const intervalId = setInterval(() => {
// use the function based set state to avoid needing count as a dependency in the useEffect.
// this stops the need to code logic around stoping and recreating the interval.
setCount(currentCount => {
console.log(`count=${currentCount}`);
return currentCount + 1;
});
}, 1000);
// Create function to clean up the interval when the component unmounts.
return () => {
if (intervalId) {
clearInterval(intervalId);
}
}
}, [setCount]);
return <div></div>;
};
ReactDOM.render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
问题是间隔只创建一次并一直指向相同的状态值。我建议 - 移动触发间隔以分隔 useEffect
,因此它在组件安装时启动。将间隔存储在变量中,以便您能够重新启动它或清除它。最后 - 每次卸载时清除它。
const App = () => {
const [count, setCount] = React.useState(0);
const [intervalSet, setIntervalSet] = React.useState(false);
React.useEffect(() => {
setIntervalSet(true);
}, []);
React.useEffect(() => {
const interval = intervalSet ? setInterval(() => {
setCount((c) => {
console.log(c);
return c + 1;
});
}, 1000) : null;
return () => clearInterval(interval);
}, [intervalSet]);
return null;
};
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div id="root"></div>
如果您需要更复杂的实现,如前所述
这确保您始终拥有最新的状态值,并且您不需要使用像 setCount(count => count + 1)
.
const useInterval = (callback, delay) => {
const savedCallback = useRef()
useEffect(() => {
savedCallback.current = callback
}, [callback])
useEffect(() => {
if (delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay])
}
// Usage
const App = () => {
useInterval(() => {
// do something every second
}, 1000)
return (...)
}
这是一个您可以使用的非常灵活的选项。但是,此挂钩 假定 您希望在组件安装时开始间隔。您的代码示例使我相信您希望它根据 intervalSet
布尔值的状态变化开始。您可以更新自定义间隔挂钩,或在您的组件中实现它。
在您的示例中看起来像这样:
const useInterval = (callback, delay, initialStart = true) => {
const [start, setStart] = React.useState(initialStart)
const savedCallback = React.useRef()
React.useEffect(() => {
savedCallback.current = callback
}, [callback])
React.useEffect(() => {
if (start && delay !== null) {
const id = setInterval(() => savedCallback.current(), delay)
return () => clearInterval(id)
}
}, [delay, start])
// this function ensures our state is read-only
const startInterval = () => {
setStart(true)
}
return [start, startInterval]
}
const App = () => {
const [countOne, setCountOne] = React.useState(0);
const [countTwo, setCountTwo] = React.useState(0);
const incrementCountOne = () => {
setCountOne(countOne + 1)
}
const incrementCountTwo = () => {
setCountTwo(countTwo + 1)
}
// Starts on component mount by default
useInterval(incrementCountOne, 1000)
// Starts when you call `startIntervalTwo(true)`
const [intervalTwoStarted, startIntervalTwo] = useInterval(incrementCountTwo, 1000, false)
return (
<div>
<p>started: {countOne}</p>
<p>{intervalTwoStarted ? 'started' : <button onClick={startIntervalTwo}>start</button>}: {countTwo}</p>
</div>
);
};
ReactDOM.render(<App />, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>