EDIT (22 June 2020): as this question has some renewed interest, I realise there may be a few points of confusion. So I would like to highlight: the example in the question is intended as a toy example. It is not reflective of the problem. The problem which spurred this question, is in the use a third party library (over which there is limited control) that takes a callback as argument to a function. What is the correct way to provide that callback with the latest state. In react classes, this would be done through the use of this. In React hooks, due to the way state is encapsulated in the functions of React.useState(), if a callback gets the state through React.useState(), it will be stale (the value when the callback was setup). But if it sets the state, it will have access to the latest state through the passed argument. This means we can potentially get the latest state in such a callback with React hooks by setting the state to be the same as it was. This works, but is counter-intuitive.
——原问题继续如下——
我正在使用React钩子,并试图从回调中读取状态。每次回调访问它时,它都会回到默认值。
使用下面的代码。控制台会一直打印Count为:0无论我点击多少次。
function Card(title) {
const [count, setCount] = React.useState(0)
const [callbackSetup, setCallbackSetup] = React.useState(false)
function setupConsoleCallback(callback) {
console.log("Setting up callback")
setInterval(callback, 3000)
}
function clickHandler() {
setCount(count+1);
if (!callbackSetup) {
setupConsoleCallback(() => {console.log(`Count is: ${count}`)})
setCallbackSetup(true)
}
}
return (<div>
Active count {count} <br/>
<button onClick={clickHandler}>Increment</button>
</div>);
}
const el = document.querySelector("#root");
ReactDOM.render(<Card title='Example Component' />, el);
你可以在这里找到这个代码
我在回调中设置状态没有问题,只是在访问最新的状态。
如果要我猜的话,我认为任何状态的改变都会创建Card函数的一个新实例。这个回调是指向旧的那个。基于https://reactjs.org/docs/hooks-reference.html#functional-updates上的文档,我有一个想法,即在回调中调用setState,并将一个函数传递给setState,以查看是否可以从setState中访问当前状态。替换
setupConsoleCallback(() => {console.log(`Count is: ${count}`)})
with
setupConsoleCallback(() => {setCount(prevCount => {console.log(`Count is: ${prevCount}`); return prevCount})})
你可以在这里找到这个代码
这种方法也没有奏效。
编辑:实际上第二种方法是可行的。我刚才回试的时候打错了。这是正确的做法。我需要调用setState来访问之前的状态。即使我无意设置状态。
我觉得我对React类采取了类似的方法,但是。为了代码的一致性,我需要坚持使用React Effects。
如何从回调中访问最新的状态信息?
不要尝试在回调中访问最近的状态,而是使用useEffect。使用setState返回的函数设置状态不会立即更新您的值。状态更新是批处理和更新的
如果您将useEffect()看作是setState的第二个参数(来自基于类的组件),这可能会有所帮助。
如果你想用最近的状态做一个操作,使用useEffect(),当状态改变时,它会被击中:
const {
useState,
useEffect
} = React;
function App() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count-1);
const increment = () => setCount(count+1);
useEffect(() => {
console.log("useEffect", count);
}, [count]);
console.log("render", count);
return (
<div className="App">
<p>{count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( < App / > , rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
更新
你可以为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(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
function Card(title) {
const [count, setCount] = useState(0);
const callbackFunction = () => {
console.log(count);
};
useInterval(callbackFunction, 3000);
useEffect(()=>{
console.log('Count has been updated!');
}, [count]);
return (<div>
Active count {count} <br/>
<button onClick={()=>setCount(count+1)}>Increment</button>
</div>);
}
const el = document.querySelector("#root");
ReactDOM.render(<Card title='Example Component'/>, el);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
关于useEffect()的更多信息
不要尝试在回调中访问最近的状态,而是使用useEffect。使用setState返回的函数设置状态不会立即更新您的值。状态更新是批处理和更新的
如果您将useEffect()看作是setState的第二个参数(来自基于类的组件),这可能会有所帮助。
如果你想用最近的状态做一个操作,使用useEffect(),当状态改变时,它会被击中:
const {
useState,
useEffect
} = React;
function App() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count-1);
const increment = () => setCount(count+1);
useEffect(() => {
console.log("useEffect", count);
}, [count]);
console.log("render", count);
return (
<div className="App">
<p>{count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( < App / > , rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
更新
你可以为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(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
function Card(title) {
const [count, setCount] = useState(0);
const callbackFunction = () => {
console.log(count);
};
useInterval(callbackFunction, 3000);
useEffect(()=>{
console.log('Count has been updated!');
}, [count]);
return (<div>
Active count {count} <br/>
<button onClick={()=>setCount(count+1)}>Increment</button>
</div>);
}
const el = document.querySelector("#root");
ReactDOM.render(<Card title='Example Component'/>, el);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
关于useEffect()的更多信息
我遇到了一个类似的错误,试图做完全相同的事情,你在你的例子-使用setInterval回调引用React组件的道具或状态。
希望我可以从一个稍微不同的方向来解决这个问题,从而为这里已经给出的好的答案补充补充——认识到这甚至不是一个React问题,而是一个普通的Javascript问题。
我认为这里最吸引人的是从React钩子模型的角度来思考的,其中的状态变量,毕竟只是一个局部变量,可以被视为React组件上下文中的有状态变量。你可以确信,在运行时,变量的值将始终是React为特定状态块所持有的任何东西。
然而,一旦你跳出React组件上下文——例如在setInterval函数中使用该变量,抽象就会中断,你就会回到那个状态变量实际上只是一个保存值的局部变量的事实。
The abstraction allows you to write code as if the value at runtime will always reflect what's in state. In the context of React, this is the case, because what happens is whenever you set the state the entire function runs again and the value of the variable is set by React to whatever the updated state value is. Inside the callback, however, no such thing happens - that variable doesn't magically update to reflect the underlying React state value at call time. It just is what it is when the callback was defined (in this case 0), and never changes.
这就是我们得到解决方案的地方:如果局部变量指向的值实际上是一个可变对象的引用,那么事情就会改变。该值(即引用)在堆栈上保持不变,但它在堆上引用的可变值可以更改。
这就是为什么接受的答案中的技术是有效的-一个React ref恰好提供了这样一个对可变对象的引用。但我认为有必要强调的是,“React”这部分只是一个巧合。解决方案,就像问题本身一样,与React本身无关,它只是React ref碰巧是获得一个可变对象引用的一种方式。
你也可以使用,例如,一个简单的Javascript类,将它的引用保持在React状态。需要明确的是,我并不是说这是一个更好的解决方案,甚至是可取的(它可能不是!),而是用它来说明这个解决方案没有“React”方面——它只是Javascript:
class Count {
constructor (val) { this.val = val }
get () { return this.val }
update (val) {
this.val += val
return this
}
}
function Card(title) {
const [count, setCount] = React.useState(new Count(0))
const [callbackSetup, setCallbackSetup] = React.useState(false)
function setupConsoleCallback(callback) {
console.log("Setting up callback")
setInterval(callback, 3000)
}
function clickHandler() {
setCount(count.update(1));
if (!callbackSetup) {
setupConsoleCallback(() => {console.log(`Count is: ${count.get()}`)})
setCallbackSetup(true)
}
}
return (
<div>
Active count {count.get()} <br/>
<button onClick={clickHandler}>Increment</button>
</div>
)
}
const el = document.querySelector("#root");
ReactDOM.render(<Card title='Example Component' />, el);
你可以看到,通过简单地让状态指向一个引用,这不会改变,并改变引用所指向的底层值,你在setInterval闭包和React组件中都得到了你想要的行为。
同样,这不是惯用的React,但只是说明了引用是这里的终极问题。希望对大家有帮助!
我将使用setInterval()和useEffect()的组合。
setInterval() on its own is problematic, as it might pop after the component has been unmounted. In your toy example this is not a problem, but in the real world it's likely that your callback will want to mutate your component's state, and then it would be a problem.
useEffect() on its own isn't enough to cause something to happen in some period of time.
useRef() is really for those rare occasions where you need to break React's functional model because you have to work with some functionality that doesn't fit (e.g. focusing an input or something), and I would avoid it for situations like this.
您的示例没有做任何非常有用的事情,而且我不确定您是否关心计时器弹出有多规律。因此,使用这种技巧大致达到你想要的效果的最简单方法如下:
import React from 'react';
const INTERVAL_FOR_TIMER_MS = 3000;
export function Card({ title }) {
const [count, setCount] = React.useState(0)
React.useEffect(
() => {
const intervalId = setInterval(
() => console.log(`Count is ${count}`),
INTERVAL_FOR_TIMER_MS,
);
return () => clearInterval(intervalId);
},
// you only want to restart the interval when count changes
[count],
);
function clickHandler() {
// I would also get in the habit of setting this way, which is safe
// if the increment is called multiple times in the same callback
setCount(num => num + 1);
}
return (
<div>
Active count {count} <br/>
<button onClick={clickHandler}>Increment</button>
</div>
);
}
需要注意的是,如果计时器弹出,那么您将在一秒钟后单击,那么下一个日志将在前一个日志后4秒出现,因为单击时计时器将重置。
如果您想解决这个问题,那么最好的方法可能是使用Date.now()来查找当前时间,并使用新的useState()来存储您想要的下一次弹出时间,并使用setTimeout()而不是setInterval()。
这有点复杂,因为你必须存储下一个计时器弹出,但不是太糟糕。这种复杂性也可以通过简单地使用一个新函数来抽象出来。总结一下,这里有一个使用钩子启动周期计时器的安全“反应”方法。
import React from 'react';
const INTERVAL_FOR_TIMER_MS = 3000;
const useInterval = (func, period, deps) => {
const [nextPopTime, setNextPopTime] = React.useState(
Date.now() + period,
);
React.useEffect(() => {
const timerId = setTimeout(
() => {
func();
// setting nextPopTime will cause us to run the
// useEffect again and reschedule the timeout
setNextPopTime(popTime => popTime + period);
},
Math.max(nextPopTime - Date.now(), 0),
);
return () => clearTimeout(timerId);
}, [nextPopTime, ...deps]);
};
export function Card({ title }) {
const [count, setCount] = React.useState(0);
useInterval(
() => console.log(`Count is ${count}`),
INTERVAL_FOR_TIMER_MS,
[count],
);
return (
<div>
Active count {count} <br/>
<button onClick={() => setCount(num => num + 1)}>
Increment
</button>
</div>
);
}
只要您在deps数组中传递interval函数的所有依赖项(与useEffect()完全一样),您就可以在interval函数中做任何您喜欢的事情(设置状态等),并且确信没有任何东西会过时。