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

时间:2019-09-09 03:38:16

标签: javascript reactjs react-hooks

我正在使用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函数的新实例。而且回调是指旧的回调。根据{{​​3}}上的文档,我想到了一种在回调中调用setState并将函数传递给setState的方法,以查看是否可以从setState中访问当前状态。更换

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

使用

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

您可以找到此代码https://reactjs.org/docs/hooks-reference.html#functional-updates

该方法也不起作用。 编辑:实际上,第二种方法确实有效。我刚在回调中输入错误。这是正确的方法。我需要调用setState来访问以前的状态。即使我无意设置状态。

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

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

9 个答案:

答案 0 :(得分:34)

对于您的方案(无法继续创建新的回调并将其传递给第三方库),可以使用templates/header.html:8:68 Block closures should also have names `{% endblock %}` 来使可变对象保持当前状态。像这样:

useRef

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

答案 1 :(得分:6)

您可以使用setState

来获取最新值。

例如

var [state,setState]=useState(defaultValue)

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

答案 2 :(得分:4)

我遇到了类似的错误,试图做与您的示例完全相同的操作-在回调中使用setInterval,该回调引用了React组件中的propsstate

希望我可以从一个略有不同的方向来解决这个问题,从而增加已经存在的答案-意识到这甚至不是一个React问题,而是一个普通的Javascript问题。

我认为,从React钩子模型的角度出发,在这里,状态变量(毕竟只是局部变量)可以被视为在React组件的上下文中是有状态的。您可以确定,在运行时,变量的值将始终是React在特定状态下持有的值。

但是,一旦您脱离React组件上下文-例如在setInterval内的函数中使用变量,抽象就会中断,您又回到了状态变量确实是只是一个拥有值的局部变量。

抽象允许您编写代码,就像一样,运行时的值将始终反映状态。在React的情况下就是这种情况,因为发生的情况是,只要您设置状态,整个函数就会再次运行,并且React会将变量的值设置为更新后的状态值。但是,在回调内部没有发生这样的事情-该变量不会神奇地更新以在调用时反映基础的React状态值。正是定义了回调(在这种情况下为0),并且永不更改的情况。

这是解决问题的地方:如果该局部变量所指向的值实际上是对可变对象的引用,那么情况就会改变。该值(作为参考)在堆栈上保持不变,但是可以更改其在堆 上引用的可变值。

这就是为什么接受的答案中的技术起作用的原因-React ref正是提供了对可变对象的引用。但是我认为必须强调,“反应”部分仅仅是一个巧合,这一点非常重要。像问题一样,解决方案与React本身无关,只是React ref恰好是获取对可变对象的引用的一种方法。

例如,您还可以使用纯Javascript类,将其引用保持在React状态。明确地说,我并不是说这是一个更好的解决方案,甚至不是可取的(它可能不是!),而是用它来说明该解决方案没有“反应”方面的观点-只是Javascript:< / p>

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

您可以看到,只需将状态指向不变的引用,然后将引用所指向的基础值进行变异,即可在{{1} }并在React组件中。

同样,这不是惯用的React,而是在这里说明了引用是最终问题的要点。希望对您有所帮助!

答案 3 :(得分:2)

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

答案 4 :(得分:0)

我知道的唯一方法是调用setState(current_value => ...)函数并在ur逻辑中使用current_value。只要确保您将其退回即可。例如:

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

答案 5 :(得分:0)

我将结合使用var data=[ "2020-06-20T11:18:40.359Z", "2020-06-15T11:17:45.511Z", "2020-05-13T11:19:45.511Z", "2019-04-20T11:49:27.828Z"]; var result = data.reduce((acc, elem)=>{ const [year, month, day] = [new Date(elem).getFullYear(), new Date(elem).getMonth(), new Date(elem).getDate()]; acc[year] = acc[year] || {}; acc[year][month] = [...(acc[year][month] || []), day]; return acc; },{}); console.log(result);setInterval()

  • useEffect()本身是有问题的,因为在卸载组件后它可能会弹出。在您的玩具示例中,这不是问题,但在现实世界中,您的回调很可能希望改变组件的状态,然后就会成为问题。
  • setInterval()本身不足以在一段时间内导致某些事情发生。
  • useEffect()确实适合那些需要打破React功能模型的罕见情况,因为您必须使用某些不合适的功能(例如,集中输入等),因此我会避免使用它这样的情况。

您的示例没有做任何非常有用的事情,并且我不确定您是否关心计时器弹出的频率如何。因此,使用此技术大致实现所需目标的最简单方法如下:

useRef()

需要注意的是,如果弹出计时器,则单击一秒钟后,下一个日志将在上一个日志之后4秒钟,因为单击时会重置计时器。

如果您想解决该问题,那么最好的办法是使用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> ); } 查找当前时间,并使用新的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一样),就可以在间隔函数中做任何您想做的事情(设置状态等)。 ),并确保一切都不会过时。

答案 6 :(得分:0)

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

实时演示

Edit GetLatestState

答案 7 :(得分:0)

onClick={() => { clickHandler(); }}

通过这种方式,您可以在单击函数时运行定义的函数,而不是在声明onClick处理程序时运行函数。

React每次发生更改时都会重新运行hook函数,当更改时,它会重新定义您的clickHandler()函数。

为了记录,您可以清理它。由于您不在乎箭头函数返回的内容,因此可以这样编写。

onClick={e => clickHandler()}

答案 8 :(得分:0)

我真的很喜欢@davnicwil 的回答,希望通过 Venue.near([40.71, -100.23], 50, latitude: :latitude3, longitude: :longitude3) 的源代码,他的意思可能会更清楚。

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() } 以数字或字符串开头,例如。 1.

initialValue

演练

  1. 第一次运行 const Component = () => { const [state, setState] = useState(initialValue) } useState = [1, ]
  2. 第二次运行 this.argsuseState 没有变化
  3. 如果用 2 调用 this.argssetState = [2, ]
  4. 下次运行 this.args 时,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值;
  • 或者设计另一个挂钩。

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