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。

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


当前回答

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模块。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回调中访问最新的状态。但意图并不清楚,我们永远不想在这种情况下使用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();

现场演示

我真的很喜欢@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,这个。Args = [1,] 你第二次运行useState,这个。Args没有变化 如果setState被2调用,这个。Args = [2,] 下次你运行useState时,这个。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)

您将得到一个“旧”值,因为您首先将当前副本(当时)传递给它。虽然这。Args每次都会获得最新的副本,这并不意味着旧的副本会被更改。您传递的值不是基于引用的。这可以成为一个特色!!

总结

为了改变它,

使用该值而不传递它; 使用对象作为值; 使用useRef获取最新值; 或者设计另一个钩子。

虽然上述方法都可以修复它(在其他答案中),但问题的根本原因是您将旧值传递给函数,并期望它与未来值一起运行。我认为这是它一开始出错的地方,如果你只看解就不太清楚。

对于您的场景(您不能一直创建新的回调并将它们传递给第三方库),您可以使用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>);

}

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

我也有类似的问题,但在我的情况下,我使用redux与钩子,也没有在同一个组件中改变状态。我有方法在回调使用的状态值分数和它(回调)有旧的分数。 我的解决方法很简单。它不像以前的那样优雅,但在我的情况下,它做到了,所以我把它放在这里,希望它能帮助到别人。

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

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

一般思想是将最新状态存储在ref:)

我将使用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函数中做任何您喜欢的事情(设置状态等),并且确信没有任何东西会过时。