繁体   English   中英

React hooks:从回调中访问最新的 state

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

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

-- 原始问题在下面继续 --

我正在使用 React 挂钩并尝试从回调中读取 state。 每次回调访问它时,它都会返回其默认值。

使用以下代码。 无论我点击多少次,控制台都会继续打印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);

您可以在此处找到此代码

我在回调中设置 state 没有问题,仅在访问最新的 state 时。

如果我猜测一下,我认为 state 的任何更改都会创建卡 function 的新实例。 并且回调指的是旧的。 根据https://reactjs.org/docs/hooks-reference.html#functional-updates上的文档,我想到了在回调中调用 setState 的方法,并将 function 传递给 setState,看看是否我可以从 setState 中访问当前的 state。 更换

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

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

您可以在此处找到此代码

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

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

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

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

}

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

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

故宫包反应,useStateRef可以访问最新的状态(如ref ),通过使用useState

2020 年 12 月更新:

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

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

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

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

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

抽象允许您编写代码,就好像运行时的值将始终反映状态一样。 在 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);

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

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

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

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

如果要使用最新状态执行操作,请使用useEffect() ,它会在状态更改时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>

关于useEffect()更多信息

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

现场演示

编辑 GetLatestState

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

演练

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

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

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

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

我会使用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()完全一样),您就可以在区间函数中执行任何您喜欢的操作(设置状态等),并且确信不会有任何结果日期。

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

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

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

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

onClick={e => clickHandler()}

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

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

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

总体思路是将最新状态存储在 ref 中:)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM