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

IT技术 javascript reactjs react-hooks
2021-03-16 17:03:57

编辑(2020 年 6 月 22 日):由于这个问题引起了一些新的兴趣,我意识到可能存在一些混淆点。所以我想强调:问题中的例子是一个玩具例子。它不能反映问题。引发这个问题的问题是使用第三方库(对其进行有限控制),该库将回调作为函数的参数。为该回调提供最新状态的正确方法是什么。在 React 类中,这将通过使用this. 在 React hooks 中,由于 state 被封装在 的函数中的方式React.useState(),如果回调通过获取状态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);

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

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

如果让我猜一猜,我会认为任何状态更改都会创建 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})})

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

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

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

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

6个回答

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

}

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

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

2021 年 6 月更新:

使用 NPM modulereact-usestateref始终获取最新的状态值。它与 React useStateAPI完全向后兼容

示例代码如何使用它:

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允许您ref使用useState.

2020 年 12 月更新:

为了完全解决这个问题,我为此创建了一个 react module。react-usestateref(react useStateRef)。例如使用:

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

它的工作原理很像,useState但此外,它还为您提供了当前状态ref.current

学到更多:

原答案

您可以使用 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 钩子。:)
2021-05-10 17:03:57
eg 部分的语法是否正确?
2021-05-20 17:03:57

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

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

我认为这里最引人注目的是 React hooks 模型的思考,其中状态变量,毕竟只是一个局部变量,可以被视为在 React 组件的上下文中它是有状态的。您可以确定,在运行时,变量的值将始终是 React 为该特定状态保持在后台的任何值。

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

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

这里是我们得到解决方案的地方:如果该局部变量指向的值实际上是可变对象引用,那么事情就会改变。的值(其为参考)保持在堆栈上恒定的,而是通过将其在堆上引用的可变的值(一个或多个)被改变。

这就是已接受答案中的技术有效的原因 - 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);

你可以看到,只要让 state 指向一个引用,它不会改变,并且改变引用指向的底层值,你就会在setInterval闭包和 React 组件中获得你所追求的行为

同样,这不是惯用的 React,而只是说明了引用是这里的最终问题。希望它有帮助!

不要尝试访问回调中的最新状态,而是使用useEffect. 使用返回的函数设置您的状态setState不会立即更新您的值。状态更新是批量更新的

如果您想到useEffect()likesetState的第二个参数(来自基于类的组件),这可能会有所帮助

如果你想用最近的状态做一个操作,使用useEffect()which 会在状态改变时被命中:

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()

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

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

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