React hooks: accessing up-to-date state from within a callback

rod*_*rod 7 javascript reactjs react-hooks

I am using React hooks and trying to read state from within a callback. Every time the callback accesses it, it's back at its default value.

With the following code. The console will keep printing Count is: 0 no matter how many times I click.

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);
Run Code Online (Sandbox Code Playgroud)

You can find this code here

I've had no problem setting state within a callback, only in accessing the latest state.

If I was to take a guess, I'd think that any change of state creates a new instance of the Card function. And that the callback is referring to the old one. Based on the documentation at https://reactjs.org/docs/hooks-reference.html#functional-updates, I had an idea to take the approach of calling setState in the callback, and passing a function to setState, to see if I could access the current state from within setState. Replacing

setupConsoleCallback(() => {console.log(`Count is: ${count}`)})
Run Code Online (Sandbox Code Playgroud)

with

setupConsoleCallback(() => {setCount(prevCount => {console.log(`Count is: ${prevCount}`); return prevCount})})
Run Code Online (Sandbox Code Playgroud)

You can find this code here

That approach hasn't worked either. EDIT: Actually that second approach does work. I just had a typo in my callback. This is the correct approach. I need to call setState to access the previous state. Even though I have no intention of setting the state.

I feel like I've taken similar approaches with React classes, but. For code consistency, I need to stick with React Effects.

How can I access the latest state information from within a callback?

Bra*_*don 93

对于您的场景(您无法继续创建新的回调并将它们传递给您的 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>);

}
Run Code Online (Sandbox Code Playgroud)

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

  • 很好的答案。这次真是万分感谢。我接受了它,因为它最适合我的场景,并且我想象了大多数现实世界的场景。为了让其他人清楚地了解为什么我更喜欢这个答案:尽管在我的示例中使用“setInterval”场景可以在每次状态更改时清除并设置一个具有更新状态的新回调,但在您无法更改正在进行的请求的回调的场景。其中大部分是异步库。而且这个解决方案简单易懂。 (6认同)
  • 引用必须在“useEffect”等副作用内发生变化。&gt; 避免在渲染期间设置引用——这可能会导致令人惊讶的行为。相反,通常您希望修改事件处理程序和效果中的引用。https://it.reactjs.org/docs/hooks-faq.html#is-there-something-like-instance-variables (4认同)
  • 我花了将近3个小时才解决这个问题。没想到这么简单。非常感谢您的回答。 (2认同)

Ami*_*ein 28

2021 年 6 月更新:

使用 NPM 模块react-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);
Run Code Online (Sandbox Code Playgroud)

NPM 包react-useStateRef允许您ref使用useState.

2020 年 12 月更新:

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

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);
Run Code Online (Sandbox Code Playgroud)

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

了解更多:

原答案

您可以使用 setState

例如:

var [state, setState, ref] = useState(0);
Run Code Online (Sandbox Code Playgroud)

  • 我认为这很完美!而不是创建对象实例和 useRef 挂钩。:) (3认同)

dav*_*wil 14

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

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

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

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

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

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

这就是已接受答案中的技术有效的原因 - React ref 提供了对可变对象的这种引用。但我认为必须强调的是,其中的“React”部分只是巧合。解决方案,就像问题一样,本身与 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);
Run Code Online (Sandbox Code Playgroud)

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

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


Nea*_*arl 10

state您可以在回调中获取最新消息setState。但意图并不明确,我们永远不想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;
}
Run Code Online (Sandbox Code Playgroud)

用法

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

...

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

// you can also use await in async callback
const counter = await getCounter();
Run Code Online (Sandbox Code Playgroud)

现场演示

编辑获取最新状态


Mir*_*ina 5

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

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

如果你想对最近的状态进行操作,请使用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);
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

更新

您可以为您创建一个钩子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);
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

一些进一步的信息useEffect()

  • 感谢您的回答@miroslav。我想我需要多解释一下:我提供的代码只是一个玩具示例,用于说明我遇到的问题。我遇到的问题是,从第三方库触发的事件需要访问最新的 React 状态。为此,我将回调传递给该库以获取状态。对于 React 类,我会将回调绑定到“this”——组件类。但是对于反应效果,我遇到了一个独特的问题,即设置回调时的状态被包含在回调中 - 导致状态过时。 (2认同)
  • 迄今为止。我找到的唯一解决方案是设置状态(即使我不想更改状态),并使用状态设置函数作为读取状态的解决方法,将“新”状态设置为与之前的状态 ```(setupConsoleCallback(() =&gt; {setCount(prevCount =&gt; {console.log(`Count is: ${prevCount}`); return prevCount})})```。但这看起来确实相反-直观。我想知道它是否还会触发对将“count”列为依赖项的任何效果进行不必要的重新渲染。尽管我对此不太确定。 (2认同)