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

Posted

技术标签:

【中文标题】React hooks:从回调中访问最新状态【英文标题】:React hooks: accessing up-to-date state from within a callback 【发布时间】:2020-01-10 20:12:17 【问题描述】:

编辑(2020 年 6 月 22 日):由于这个问题有一些新的兴趣,我意识到可能存在一些混淆点。所以我想强调一下:问题中的示例旨在作为玩具示例。它不能反映问题。引发这个问题的问题是使用第三方库(对其控制有限),该库将回调作为函数的参数。为该回调提供最新状态的正确方法是什么。在反应类中,这将通过使用this 来完成。在 React Hooks 中,由于 state 被封装在 React.useState() 的函数中的方式,如果一个回调 get 通过React.useState() 获得状态,它将是陈旧的(设置回调时的值) .但是如果它设置状态,它将可以通过传递的参数访问最新状态。这意味着我们可以通过将状态设置使其与原来的状态相同,从而在使用 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);

你可以找到这个代码here

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

如果我猜测一下,我认为任何状态变化都会创建 Card 函数的新实例。并且回调指的是旧的。根据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))

你可以找到这个代码here

这种方法也没有奏效。 编辑:实际上第二种方法确实有效。我只是在我的回调中有一个错字。这是正确的做法。我需要调用 setState 来访问之前的状态。即使我无意设置状态。

我觉得我对 React 类采取了类似的方法,但是。为了代码的一致性,我需要坚持使用 React Effects。

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

【问题讨论】:

useState set method not reflecting change immediately的可能重复 我不相信它是上述的副本。因为它不是关于设置状态是异步的事实。但是关于状态 forever 在回调中是陈旧的。现在我找到了答案,但它可能与***.com/questions/56782079/react-hooks-stale-state 重复。然而,我认为这指向了一个有趣的结果,即反应钩子如何管理状态。即您需要调用设置状态的函数,以便在回调中访问正确的状态。即使您不打算更改状态,这仍然是正确的。 是的,你是对的,它是关于函数闭包的工作原理。这是***.com/questions/57471987/… 的主题之一 也许 Dan Abramov 的这篇博客会有所帮助:overreacted.io/making-setinterval-declarative-with-react-hooks,它解释了为什么混合使用 hooks 和 setInterval 确实令人困惑,而且一开始似乎不起作用。 TL:DR 基本上是因为关闭,您需要用下一个状态值“重新封装”回调。 是的,我认为它是这样的,因为如果我使用类,我会将this 绑定到回调,但看不到如何使用效果。我尝试了一些方法,例如将状态变量的 getter 作为回调的参数。但没有任何效果。无论如何,在查看了每个人共享的所有链接之后,我仍然不清楚。除了调用它的状态设置函数(特别是当我不想想改变它的状态时),有没有办法从另一个上下文中读取组件的状态? 【参考方案1】:

对于您的场景(您无法继续创建新的回调并将它们传递给您的第 3 方库),您可以使用 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>);


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

【讨论】:

优秀的答案。这次真是万分感谢。我接受了它,因为它最适合我的场景,我想象大多数现实世界的场景。为了让其他人清楚我为什么更喜欢这个答案:虽然在我的示例中使用setInterval 场景可以在每次状态更改时清除和设置具有更新状态的新回调,但这并不容易完成您无法更改正在进行的请求的回调的情况。其中有大多数异步库。而且这个解决方案简单易懂。 引用必须在像useEffect这样的副作用中发生变异。 > 避免在渲染期间设置 refs——这可能会导致令人惊讶的行为。相反,通常您希望修改事件处理程序和效果中的 refs。 it.reactjs.org/docs/… @daphtdazz 我原则上同意你的看法。然而,在这种情况下,我正在处理需要读取最新状态的持续、长时间运行的回调(提供给外部库)。这本身并不是很像 React,但我们可能别无选择。如果我们接受我们无法删除并重新建立此回调,则让回调保持“指向”最新状态的可变引用似乎是一致的(不仅仅是让回调使用setState 来读取状态,这只是隐藏了问题)。当然,理想情况下,我们一开始就不会处于这种情况。 我花了将近 3 个小时来解决这个问题。没想到这么简单。非常感谢您的回答。 @Jack 我的结论是,如果你需要状态可以被 React 上下文和组件生命周期之外的东西读取,那么 useRef 是正确的方法。如果需要在 React 之外访问状态,则将可变引用传递给状态是一致的(至少对我而言,其他人可能不同意)。但是,如果您尝试从 React 应用程序中读取最新状态,setState 是一个简单而好的解决方案。但在大多数此类情况下,如果出现此要求,您可能不会以非常类似 React 的方式做事。因此,可能值得回顾一下整体方法。【参考方案2】:

2021 年 6 月更新:

使用 NPM 模块react-usestateref 始终获取最新的状态值。它与 React useState 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);

NPM 包react-useStateRef 允许您使用useState 访问最新状态(如ref)。

2020 年 12 月更新:

为了解决这个问题,我为此创建了一个反应模块。 react-usestateref(反应 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.
)

【讨论】:

我认为这是完美的!而不是创建对象实例和 useRef 挂钩。 :) 例如部分的语法是否正确?【参考方案3】:

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

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

我认为这里吸引人的是从 React 钩子模型的角度思考,其中状态变量,毕竟只是一个局部变量,可以被视为在 React 组件的上下文中是有状态的。您可以确定,在运行时,变量的值将始终是 React 为该特定状态保留的任何内容。

然而,一旦你脱离 React 组件上下文——例如,在 setInterval 内的函数中使用变量,抽象就会中断,你就会回到这个状态变量真的只是一个保存值的局部变量。

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

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

这就是为什么接受的答案中的技术有效的原因 - 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,只是说明了引用是这里的最终问题。希望对您有所帮助!

【讨论】:

感谢您的解释!完全为我澄清了。【参考方案4】:

不要尝试访问回调中的最新状态,而是使用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>

Some further info on useEffect()

【讨论】:

感谢您的回答@miroslav。我想我需要再解释一下:我提供的代码只是一个玩具示例,用于说明我遇到的问题。我遇到的问题是从第三方库触发的事件需要访问最新的 React 状态。为此,我将一个回调传递给该库,以便它可以获取状态。使用反应类,我会将回调绑定到this - 组件类。但是对于反应效果,我有一个独特的问题,即设置回调时的状态被包含在回调中 - 导致过时状态。 到目前为止。我找到的唯一解决方案是设置状态(即使我不想更改状态),并使用状态设置功能作为读取状态的解决方法,将“新”状态设置为与之前的状态(setupConsoleCallback(() =&gt; setCount(prevCount =&gt; console.log(`Count is: $prevCount`); return prevCount))。但这确实似乎违反直觉。我想知道它是否还会触发对将 count 列为依赖项的任何效果进行不必要的重新渲染。虽然我不太确定这一点。 @rod 遇到了同样的问题,处理对另一个库的回调。除了设置状态以读取值之外,您是否都想出了其他方法? @Tom 不,我坚持使用我在问题末尾提供的方法:使用状态挂钩中的 setter 函数读取最新状态(从传递的变量中)而不实际更改它。现在可能有更好的方法可以做到这一点,但当时我认为这是最好的方法。【参考方案5】:

我知道的唯一方法是调用 setState(current_value => ...) 函数并在你的逻辑中使用 current_value。只要确保你把它退回来。例如:

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

【讨论】:

【参考方案6】:

您可以在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();

现场演示

【讨论】:

【参考方案7】:

我真的很喜欢@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)
  

演练

    第一次运行 useState, this.args = [1, ]​​i> 第二次运行useStatethis.args没有变化 如果 setState 使用 2 调用,this.args = [2, ]​​i> 下次运行useStatethis.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值; 或者设计另一个钩子。

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

【讨论】:

老实说,在我看来,这个答案比选择的答案更清楚。我一直在为为什么会在我的应用程序中发生这种情况而苦苦挣扎,并因此而把我的头发拉了出来。当组件重新渲染时,传递给函数参数的状态变量是否有任何原因没有得到更新?【参考方案8】:

我会使用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>
  );

只要你在deps数组中传递区间函数的所有依赖项(与useEffect()完全一样),你就可以在区间函数中做任何你喜欢的事情(设置状态等)并且有信心什么都不会过时。

【讨论】:

【参考方案9】:
onClick=() =>  clickHandler(); 

这样,您在单击它时而不是在声明 onClick 处理程序时运行定义的函数。

React 每次发生变化时都会重新运行钩子函数,当它这样做时,它会重新定义你的 clickHandler() 函数。

为了记录,你可以清理它。因为你不关心你的箭头函数返回什么,你可以这样写。

onClick=e => clickHandler()

【讨论】:

【参考方案10】:

我遇到了类似的问题,但在我的情况下,我使用的是带有钩子的 redux,而且我没有在同一个组件中改变状态。我有在回调中使用状态值score 的方法,它(回调)有旧分数。 所以我的解决方案很简单。它不像以前的那样优雅,但就我而言,它完成了这项工作,所以我把它放在这里,希望它会对某人有所帮助。

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

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

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

【讨论】:

以上是关于React hooks:从回调中访问最新状态的主要内容,如果未能解决你的问题,请参考以下文章

如何访问 setState 回调之外的最新状态?

在hook里面使用回调refs

不能在回调中调用 React Hook “useState”

错误:无法在回调中调用 React Hook “useLogout”。阿波罗

React+Typescript:修复 UseEffect Hook(回调+清理函数错误)

React Hook 下setState的回调