React钩子:从回调中访问最新状态

编辑(2020年6月22日):由于这个问题重新引起了一些兴趣,我意识到可能会有一些困惑。所以我想强调:问题中的例子是一个玩具的例子。这并不能反映问题。引发这个问题的问题是使用第三方库(对其控制有限),该库将回调作为函数的参数。为回调提供最新状态的正确方法是什么?在REACT类中,这将通过使用this来完成。在React钩子中,由于状态封装在React.useState()的函数中的方式,如果回调通过React.useState()得到状态,则它将是STALE(设置回调时的值)。但是如果它__状态ABC4,它将通过传递的参数访问最新的状态。这意味着我们可以通过设置,使用React钩子潜在地获得回调中的最新状态,该状态与原来的状态相同。这是可行的,但与直觉相反。

--原始问题在下面继续--

我正在使用React钩子并尝试从回调中读取状态。每次回调访问它时,它都会返回其默认值。

使用下面的代码。无论我单击多少次,控制台都将继续打印Count is: 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);

你可以在这里找到这个代码。

我在回调中设置状态时没有遇到问题,只是在访问最新状态时遇到了问题。

如果让我猜的话,我会认为任何状态的改变都会创建一个新的卡片函数实例。回调引用的是旧的。根据https://reactjs.org/docs/hooks-reference.html#functional-updates上的文档,我有一个想法,即采用在回调中调用SetState的方法,并将一个函数传递给SetState,以查看是否可以从SetState中访问当前状态。正在替换

setupConsoleCallback(() => {console.log(`Count is: ${count}`)})

setupConsoleCallback(() => {setCount(prevCount => {console.log(`Count is: ${prevCount}`); return prevCount})})

你可以在这里找到这个代码。

这种方法也没有奏效。 编辑:实际上,第二种方法确实工作。我刚刚在回拨中打错了一个字。这是正确的做法。我需要调用SetState来访问以前的状态。尽管我无意设定状态。

我觉得我在React类中采用了类似的方法,但是。为了保持代码的一致性,我需要坚持使用React效果。

如何从回调中访问最新的状态信息?

83696 次浏览

使用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()更多信息

我知道的唯一方法是调用setState(当前_value=>..)函数,并在UR逻辑中使用当前_的值。一定要把它还回去。例句:

const myPollingFunction = () => {
setInterval(() => {
setState(latest_value => {
// do something with latest_value
return latest_value;
}
}, 1000);
};

对于您的场景(您不能不断创建新的回调并将它们传递给第三方库),您可以使用useRef来保持可变对象的当前状态。像这样:

function Card(title) {
const [count, setCount] = React.useState(0)
const [callbackSetup, setCallbackSetup] = React.useState(false)
const stateRef = useRef();


// make stateRef always have the current count
// your "fixed" callbacks can refer to this object whenever
// they need the current value.  Note: the callbacks will not
// be reactive - they will not re-run the instant state changes,
// but they *will* see the current value whenever they do run
stateRef.current = count;


function setupConsoleCallback(callback) {
console.log("Setting up callback")
setInterval(callback, 3000)
}


function clickHandler() {
setCount(count+1);
if (!callbackSetup) {
setupConsoleCallback(() => {console.log(`Count is: ${stateRef.current}`)})
setCallbackSetup(true)
}
}




return (<div>
Active count {count} <br/>
<button onClick={clickHandler}>Increment</button>
</div>);


}

您的回调可以引用可变对象来“读取”当前状态。它将在其闭包中捕获可变对象,并且每次呈现该可变对象时都将使用当前状态值进行更新。

我遇到了一个类似的bug,它试图做与您在示例中所做的完全相同的事情-在从React组件引用propsstate的回调上使用setInterval

希望我可以通过从一个稍微不同的方向来解决这个问题来补充这里已经存在的好答案——意识到这甚至不是一个反应问题,而是一个普通的JavaScript问题。

我认为这里要考虑的是React钩子模型,其中状态变量,毕竟只是一个局部变量,可以被视为在React组件的上下文中是有状态的。可以肯定的是,在运行时,变量的值将始终是特定状态下React所保存的值。

然而,一旦你打破了React组件的上下文——例如,在setInterval中的函数中使用变量,抽象就被打破了,你又回到了状态变量实际上只是一个保存值的局部变量的事实。

抽象允许您编写代码好像运行时的值将始终反映状态中的内容。在React的上下文中,情况就是这样,因为每当您设置状态时,整个函数都会再次运行,并且无论更新的状态值是什么,React都会将变量的值设置为。然而,在回调中,不会发生这样的事情-该变量不会神奇地更新以反映调用时的底层React状态值。它就是定义回调时的样子(在本例中是0),永远不会改变。

这就是我们的解决方案:如果该局部变量指向的值实际上是可变对象的__abc0,那么情况就会发生变化。该值(即引用)在堆栈上保持不变,但它在堆上引用的可变值可以更改。

这就是为什么Accepted Answer中的技术是有效的——React Ref提供了对可变对象的精确引用。但我认为强调“反应”部分只是一个巧合是非常重要的。解决方案和问题一样,与React本身无关,只是React ref恰好是获取对可变对象的引用的一种方法。

例如,您还可以使用一个普通的JavaScript类,在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()本身是有问题的,因为它可能会在卸载组件后弹出。在您的玩具示例中,这不是一个问题,但在现实世界中,您的回调可能会改变组件的状态,这将是一个问题。
  • useEffect()本身不足以在一段时间内导致某些事情发生。
  • useRef()实际上是为那些罕见的情况,你需要打破React的功能模型,因为你必须使用一些不适合的功能(例如,关注输入或其他东西),我会避免这种情况。

你的例子没有做任何有用的事情,我不确定你是否关心计时器的规律。因此,使用此技术大致实现所需的最简单方法如下:

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>
);
}

只要在__abc0数组中传递interval函数的所有依赖项(与__abc1完全相同),您就可以在interval函数中执行任何操作(设置状态等),并且确信没有任何内容会过时。

2021年6月

更新:

使用NPM模块react-usestateref始终获取最新的状态值。它与ReactuseState API完全向后兼容。

示例代码如何使用它:

import useState from 'react-usestateref';


const [count, setCount, counterRef] = useState(0);


console.log(couterRef.current); // it will always have the latest state value
setCount(20);
console.log(counterRef.current);

通过使用useState,npm包反应-使用状态参考允许您访问最新状态(如ref)。

2020年12月

更新:

为了解决这个问题,我创建了一个React模块,react-usestateref(React UseStateRef)。例如使用:

var [state, setState, ref] = useState(0);

它的工作方式与useState完全相同,但除此之外,它还为您提供ref.current下的当前状态。

了解更多:

  • https://www.npmjs.com/package/react-usestateref.

原来的答案

您可以使用setState获取最新值

例如:

var [state, setState] = useState(defaultValue);


useEffect(() => {
var updatedState;
setState(currentState => { // Do not change the state by getting the updated state
updateState = currentState;
return currentState;
})
alert(updateState); // the current state.
})

您可以在setState回调中访问最新的state。但意图并不明确,我们永远不想在这种情况下setState,它可能会使其他人在阅读您的代码时感到困惑。所以你可能想用另一个钩子把它包起来,这样可以更好地表达你想要的东西。

function useExtendedState<T>(initialState: T) {
const [state, setState] = React.useState<T>(initialState);
const getLatestState = () => {
return new Promise<T>((resolve, reject) => {
setState((s) => {
resolve(s);
return s;
});
});
};


return [state, setState, getLatestState] as const;
}

用法

const [counter, setCounter, getCounter] = useExtendedState(0);


...


getCounter().then((counter) => /* ... */)


// you can also use await in async callback
const counter = await getCounter();

现场演示

Edit GetLatestState

我真的很喜欢@davnicwil的回答,希望有了useState的源代码,他的意思可能会更清楚。

  // once when the component is mounted
constructor(initialValue) {
this.args = Object.freeze([initialValue, this.updater]);
}


// every time the Component is invoked
update() {
return this.args
}


// every time the setState is invoked
updater(value) {
this.args = Object.freeze([value, this.updater]);
this.el.update()
}


在用法中,如果initialValue以数字或字符串开头,例如。1.

  const Component = () => {
const [state, setState] = useState(initialValue)
}

走查

  1. 第一次运行useState时,this.args=[1,]
  2. 第二次运行useState时,this.args没有变化
  3. 如果使用2调用setState,则this.args=[2,]
  4. 下次运行useState时,this.args没有变化

现在,如果你做了一些事情,特别是对值的延迟使用。

  function doSomething(v) {
// I need to wait for 10 seconds
// within this period of time
// UI has refreshed multiple times
// I'm in step 4)
console.log(v)
}


// Invoke this function in step 1)
doSomething(value)

你会得到一个“老”值,因为您在第一个位置将当前副本(当时)传递给它。虽然this.args每次都会得到最新的副本,但这并不意味着旧的副本会被更改。您传递的值不是基于引用的。这可能是一个功能!

总结

为了改变它,

  • 使用该值而不传递它;
  • 使用object作为值;
  • 利用useRef得到latest值;
  • 或者设计另一个钩子。

尽管上面的方法解决了这个问题(在其他答案中),但问题的根本原因是您将旧值传递给函数,并期望它使用未来的值运行。我认为这是它最初出错的地方,如果你只看解决方案,这并不是很清楚。

我遇到了类似的问题,但在我的例子中,我使用了带有钩子的Redux,而且我没有在同一组件中改变状态。我有一个方法,在回调时使用状态值score,它(回调)有旧的分数。 所以我的解决方案很简单。它不像以前的那些那么漂亮,但在我的情况下,它完成了工作,所以我把它放在这里,希望它能帮助别人。

const score = useSelector((state) => state.score);
const scoreRef = useRef(score);


useEffect(() => {
scoreRef.current = score ;
}, [score])

一般的想法是将最新的状态存储在REF中:)