【问题标题】:React state inside a function is not changing even after calling it with a delay of (5 seconds)即使延迟(5 秒)调用函数后,函数内部的反应状态也不会改变
【发布时间】:2020-06-26 01:37:17
【问题描述】:

在反应中我正在使用功能组件,我有两个功能(getBooks)和(loadMore)

getBooks 从端点获取数据。但是,当我在getBooks 函数(loadMoreClicked)内单击按钮时调用loadMore 函数时,即使在延迟(5 秒)后调用它,它也会使用以前的状态。但是当我再次调用loadMore 时,状态发生了变化,一切正常。

有人可以解释为什么最初调用 (getBooks) 时的 (loadMoreClicked) 没有更新 甚至在延迟 5 秒后调用它。

function component() {
  const [loadMoreClicked, setLoadMore] = useState(false);
  const getBooks = () => {
    const endPoint = `http://localhost/getBooks`; //this is my end point
    axios
      .get(endPoint, {
        params: newFilters
      })
      .then(res => {
        console.log(loadMoreClicked); //the (loadMoreClicked) value is still (false) after (5 sec)
      })
      .catch(err => {
        console.log(err);
      });
  };

  const loadMore = () => {
    setLoadMore(true); //here i am changing (loadMoreClicked) value to (true)

    setTimeout(() => {
      getBooks(); // i am calling (getBooks()) after 5 seconds.
    }, 5000);
  };

  return (
    <div>
      <button onClick={() => loadMore()}>loadMore</button> //calling (loadMore)
      function
    </div>
  );
}

【问题讨论】:

  • 国家不是这样运作的。状态仅在重新渲染时更新,因此loadMoreClicked 的值将始终为false,直到您的组件重新渲染。这不是等待它改变的情况。
  • 但是当我在 loadMore 函数中调用 (setLoadMore(true)) 时,状态已经改变,组件应该重新渲染并且状态应该已经更新。之后,当我在 5 秒后调用 getBooks 时,getBooks应该已经改变了可用状态
  • 如果我没记错组件应该在调用 setLoadMore(true) 时重新渲染并且状态应该已经更新。请回答
  • 否,组件不能在函数中途重新渲染,否则函数将停止执行。当你调用setLoadMore 时,你实际上并没有设置状态——你只是告诉 React 你想要设置状态。 React 将在稍后组件重新渲染时执行此操作。当您仍在函数内时,组件不会重新渲染。
  • 但是 getBooks 在 5 秒后被调用。它仍然没有对新更新值的引用。为什么会这样。该组件应该在 5 秒内重新渲染。但是 getBooks 中的 loadMoreClicked 更新值仍然为 false

标签: javascript reactjs react-functional-component use-state


【解决方案1】:

发生了两件事:

  1. getBooks() 正在使用在周围函数中定义的 const 值。当函数在其定义之外引用constlet 变量时,它会创建所谓的闭包。闭包从这些外部变量中获取值,并将值的内部函数副本提供为 它们在构建函数时的值。 在这种情况下,函数是在最初调用状态后立即构建的,将loadMoreClicked 设置为false

  2. 那么为什么setLoadMore(true) 不触发重新渲染并重写函数呢?当我们设置状态时,重新渲染不会立即发生。它被添加到 React 管理的队列中。这意味着,当执行loadMore() 时,setLoadMore(true) 会说“在我运行完其余代码后更新状态”。重新渲染发生在函数结束之后,因此使用的 getBooks() 的副本是在此循环中构建并排队的副本,其中包含内置的原始值。

对于您正在执行的操作,您可能希望在超时时调用不同的函数,具体取决于是否单击了按钮。或者,您可以创建另一个更直接的关闭,具体取决于您是否希望 getBooks() 考虑单击的按钮,如下所示:

const getBooks = wasClicked => // Now calling getBooks(boolean) returns the following function, with wasClicked frozen
  () => {
    const endPoint = `http://localhost/getBooks`;
    axios
    .get(endPoint, {
      params: newFilters
    })
    .then(res => {
      console.log(wasClicked); // This references the value copied when the inner function was created by calling getBooks()
    })
    .catch(err => {
      console.log(err);
    });
  }

...

const loadMore = () => {
  setLoadMore(true);
  setTimeout(
    getBooks(true), // Calling getBooks(true) returns the inner function, with wasClicked frozen to true for this instance of the function
    5000
  );
};

还有第三种选择,即将const [loadMoreClicked, setLoadMore] 重写为var [loadMoreClicked, setLoadMore]。虽然引用 const 变量会冻结该时刻的值,但 var 不会。 var 允许函数动态引用变量,以便在函数执行时确定值,而不是在定义函数时确定。

这听起来像是一个快速简单的解决方法,但在使用闭包(例如上面的第二种解决方案)时可能会引起混淆。在这种情况下,由于闭包的工作方式,该值再次固定。因此,您的代码将在闭包中冻结值,但不会在常规函数中冻结,这可能会导致更多混乱。

我个人的建议是保留const 定义。 var 被开发社区使用的频率较低,因为它在闭包和标准函数中的工作方式令人困惑。在实践中,大多数(如果不是全部)钩子都会填充 const。将其作为单独的 var 参考将使未来的开发人员感到困惑,他们可能会认为这是一个错误并更改它以适应模式,从而破坏您的代码。

如果您确实想动态引用loadMoreClicked 的状态,并且您不一定需要重新渲染组件,我实际上建议使用useRef() 而不是useState()

useRef 创建一个具有单个属性的对象,current,它保存您放入其中的任何值。当您更改 current 时,您正在更新可变对象上的值。因此,即使对对象的引用在时间上被冻结,它也引用了具有最新值的可用对象。

这看起来像:

function component() {
  const loadMoreClicked = useRef(false);
  const getBooks = () => {
    const endPoint = `http://localhost/getBooks`;
    axios
    .get(endPoint, {
      params: newFilters
    })
    .then(res => {
      console.log(loadMoreClicked.current); // This references the property as it is currently defined
    })
    .catch(err => {
      console.log(err);
    });
 }


  const loadMore = () => {
    loadMoreClicked.current = true; // property is uodated immediately
    setTimeout(getBooks(), 5000);
  };

}

这是可行的,因为虽然loadMoreClicked 被定义为顶部的const,但它是一个常量对对象的引用, 而不是常量值。被引用的对象可以随意改变。

这是 Javascript 中最令人困惑的事情之一,它通常在教程中被掩盖,所以除非你有一些后端经验,比如在 C 或 C++ 中使用指针,否则它会很奇怪。

因此,对于您正在做的事情,我建议使用 useRef() 而不是 useState()。如果您确实想重新渲染组件,例如,如果您想在加载内容时禁用按钮,然后在加载内容时重新启用它,我可能会同时使用它们,并重命名它们以更清楚地了解它们的用途:

function component() {
  const isLoadPending = useRef(false);
  const [isLoadButtonDisabled, setLoadButtonDisabled] = useState(false);
  const getBooks = () => {
    const endPoint = `http://localhost/getBooks`;
    axios
    .get(endPoint, {
      params: newFilters
    })
    .then(res => {
      if (isLoadPending.current) {
        isLoadPending.current = false:
        setLoadButtonDisabled(false);
      }
    })
    .catch(err => {
      console.log(err);
    });
 };

  const loadMore = () => {
    isLoadPending.current = true;
    setLoadButtonDisabled(true);
    setTimeout(getBooks(), 5000);
  };

}

它有点冗长,但它有效,并且可以将您的关注点分开。 ref 是你的标志,告诉你的组件它现在在做什么。状态指示组件应如何呈现以反映按钮。

设置状态是一种即发即弃的操作。在组件的整个功能执行之前,您实际上不会看到它的变化。请记住,在使用 setter 函数之前,您已经获得了价值。所以当你设置状态时,你并没有改变这个循环中的任何东西,你是在告诉 React 运行另一个循环。在第二个周期完成之前不渲染任何东西是足够聪明的,所以它很快,但它仍然运行两个完整的周期,从上到下。

【讨论】:

  • 但是当 getBooks 在 5 秒后调用时,它应该引用 loadMoreClick 的新值。为什么没有
  • 噢噢噢噢,我忘了引用第三个元素——事实上 getBooks() 引用了一个 const 值。我会更新我的答案。
  • 好的,所以我添加了我认为应该工作的内容,以及引用const。至于为什么它不起作用,这是因为状态更改在与超时不同的上下文中运行。您在状态为假的情况下使用 const 值创建了超时。何时执行并不重要。创建它的时间很重要。
【解决方案2】:

您可以使用useEffect 方法来监视loadMoreClicked 更新,例如componentDidUpdate 生命周期方法,并在其中调用setTimeout

useEffect(() => {
    if(loadMoreClicked){
      setTimeout(() => {
        getBooks();
      }, 5000);
    }
  }, [loadMoreClicked])

只有在 loadMoreClicked 更改为 true 之后,我们才会调用 setTimeout

这归结为闭包在 JavaScript 中的工作方式。赋予setTimeout 的函数将从初始渲染中获取loadMoreClicked 变量,因为loadMoreClicked 没有发生变异。

【讨论】:

  • 功能组件的useState钩子中是否存在回调,据我所知它只适用于类组件的setState
  • 是的,你是对的,useState 钩子中不存在回调,我现在将根据钩子编辑我的答案
  • @user8989,我使用useEffect() 来观察loadMoreClicked 的变化,看看这是否有效?
  • 但我的意思是,当我调用 setLoadMore(true) 时,组件应该在状态更改时重新渲染,并且状态应该已经更新,并且 loadMoreclicked 应该更改为 true。但即使在 getBooks() 函数中 5 秒后,loadMoreclicked 的值也没有改变
  • 那是因为setLoadMore(true) 是一个异步操作,并且您在setLoadMore(true) 正下方调用setTimeout(),所以当时loadMoreClicked 变量不会更改为truesetTimeout 将获得变量 loadMoreClicked,其值为 false,然后在 getBooks() 中引用
猜你喜欢
  • 2021-11-06
  • 1970-01-01
  • 1970-01-01
  • 2021-01-23
  • 2022-01-18
  • 2020-08-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多