【问题标题】:Better way to use useState hook for setting the boolean state in React在 React 中使用 useState 钩子设置布尔状态的更好方法
【发布时间】:2022-01-03 12:48:15
【问题描述】:

我刚刚开始学习 React 并了解了 useState 钩子。我遇到了两种设置布尔数据状态的不同方法。那么这两种方法是否相同,如果不同,应该更喜欢哪一种?

const [isChanged, setIsChanged] = useState<boolean>(false)
  
const onClick = () => {
    setIsChanged((prevState) => !prevState)  // Approach 1
    setIsChanged(!isChanged)  // Approach 2
}

【问题讨论】:

  • 第一种更安全,因为状态是异步的,第二种方法在某些情况下可能有问题。第一个状态确保它与前一个状态相反。
  • 使用第一种情况,就好像你在使用 useState 钩子一样,也因为你的新状态取决于以前的状态。
  • 文档很好地解释了这一点reactjs.org/docs/faq-state.html
  • @AlexWayne 我不认为该链接特别有用 - 它似乎只是在谈论类组件中的 setState 方法,而不是钩子。即使它们使用大致相同的底层机制,这里的问题也是特定于钩子的,因为在类组件中设置状态没有等效于“方法 1”。
  • State updates 更具体地说,functional updates 用于 useState 钩子,充分解释了方法 1 优于普通状态更新的原因。

标签: javascript reactjs typescript react-hooks use-state


【解决方案1】:

因为在代码中,一个简单的示例通常会绘制一千个单词,这里有一个简单的 CodeSandbox 演示来说明差异,以及为什么,如果您想要基于 状态值的更新更新,“更新函数”(方法一)是最好的:

https://codesandbox.io/s/stack-overflow-demo-nmjiy?file=/src/App.js

这是一个独立的 sn-p 中的代码:

<div id="root"></div><script src="https://unpkg.com/react@17.0.2/umd/react.development.js"></script><script src="https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js"></script><script src="https://unpkg.com/@babel/standalone@7.16.7/babel.min.js"></script>
<script type="text/babel" data-type="module" data-presets="env,react">

function App() {
  const [count, setCount] = React.useState(0);

  // this uses the "good way" but it doesn't really matter here
  const incrementPlain = () => setCount((oldCount) => oldCount + 1);

  const incrementWithTimeoutBad = () =>
    setTimeout(() => setCount(count + 1), 3000);
  const incrementWithTimeoutGood = () =>
    setTimeout(() => setCount((oldCount) => oldCount + 1), 3000);

  return (
    <div>
      <div>Current count: {count}</div>
      <div>
        <button onClick={incrementPlain}>
          Increment (doesn't matter which way)
        </button>
      </div>
      <div>
        <button onClick={incrementWithTimeoutBad}>
          Increment with delay (bugged)
        </button>
        <button onClick={incrementWithTimeoutGood}>
          Increment with delay (good)
        </button>
      </div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

</script>

这里我们有一个简单的数字“计数”状态,它显示在标记中,还有 3 个不同的按钮都会增加它。

上面的只是直接增加 - 我碰巧在这里使用了函数形式(“方法 1”),因为我更喜欢这种风格,原因希望能变得清晰,但正如我的评论所说,它没有在这里实际上并不重要。

以下两个使用您在问题中概述的两种不同方法,并在延迟后这样做。为了简单起见,我在这里使用setTimeout 完成了此操作 - 虽然这不是特别现实,但在动作调用 API 端点的实际应用程序中通常会看到类似的效果(尽管人们希望这通常不会被视为只要 3 秒,通过更快的请求总是可以观察到相同的问题 - 我只是放慢了速度以便更容易触发错误)。

要查看差异,请尝试对底部的 2 个按钮中的每一个进行以下操作:

  • 点击按钮
  • 在 3 秒超时之前单击顶部的按钮(再次增加计数)

您应该会看到明显的行为差异:

  • 使用“方法 1”(右侧的按钮,我在这里称其为“良好”),超时结束后计数会增加第二次。
  • 使用“方法 2”(左侧按钮,我称之为“窃听”),无论您等待多长时间,中间单击顶部按钮所产生的值都不会进一步增加李>

(如果您快速单击底部按钮多次,然后单击顶部按钮,您会看到这一点更加明显。为了获得更违反直觉的效果,请尝试按一次或多次“被窃听”的底部按钮,然后单击顶部按钮不止一次,都在 3 秒的时间间隔内。)

为什么会这样?好吧,之所以会出现“错误”行为,是因为 setTimeout 内部的函数是外部变量 count 的闭包,该变量位于完整组件函数的范围内。这意味着当以count + 1 作为参数调用它时,它会将计数更新为1,而不是定义函数时的计数。假设您从首先加载 count 为 0 的组件开始执行上述顺序,然后发生的更详细的顺序是:

  • 底部按钮单击安排回调在 3 秒后发生。由于此时count 等于0,因此它的参数count + 1 等于1。
  • 顶部按钮单击重新呈现组件,计数现在等于 1。
  • 稍后触发在第一步设置的回调,并将计数设置为 1。这不会导致任何明显的机会,因为计数已经是 1。(如果您尝试多次单击顶部按钮,那么现在显示 2 或更多,这实际上会减少计数器,因为正如我所解释的,它将始终设置为 1。)

如果你对 JS 闭包有点了解,你可能会奇怪为什么闭包中访问的count 还是 0,之前不是更新为 1 吗?不,它不是,这可能是违反直觉的。注意count 是如何用const 声明的?没错——它从来没有真正改变过。 UI 更新的原因是因为setCount 导致 React 重新渲染你的组件,这意味着与该组件对应的整个外部函数被再次调用。这设置了一个全新的环境,带有一个 new count 变量。 React 的内部确保 useState 调用现在返回 1 作为当前计数,因此这是组件的新“实例”中的值 - 但从放入事件中的函数的角度来看,这无关紧要3秒后排队开火。就它而言,计数变量 - 不再在范围内,而是在回调中“记住”,因为所有封闭变量都是 - 从未从 0 改变。等于 1 的计数完全在不同的范围内,并且永远无法访问第一个回调。

函数参数形式 - “方法 1” - 如何解决这个问题?非常简单地。它根本不包含任何闭包 - 该函数内部的变量,为了准确性和消除外部count 的歧义,我在这里将其称为oldCount - 与count 无关外部。这是 React 本身将在内部调用的函数的参数。当 React 确实调用该函数时,它总是提供它拥有的“最新”状态值。所以你不必担心“过时的闭包”或类似的事情——你说的是“无论最近的值是什么,都将计数更新为多一”,React 会处理剩下的事情。

我在这里将方法 2 称为“错误”,因为我认为如果您单击了设置为执行增量的按钮,那么在超时后发生增量是合理的。但这并不总是你想要的。如果您真的希望更新基于第一次单击按钮时的值,那么您当然会更喜欢方法 2,而方法 1 似乎有问题。从某种意义上说,情况更是如此。我强烈推荐阅读 Dan Abramov 的 this 帖子——核心 React 开发人员之一——它解释了类组件和函数之间的关键区别,它基于许多关于闭包的相同论点,这些论点在这里发挥作用,通常你 确实希望事件处理程序引用渲染时的值,而不是在 API 请求或超时后实际触发时。

但是那篇文章与状态更新函数的“方法 1”形式没有任何关系,文章中甚至都没有提到。那是因为它与给出的示例无关 - 没有(明智的)方法来重写这些示例以使用它。但是当您确实想要更新状态值时基于其先前的值 - 可能会发生在 OP 示例中否定布尔值或将计数器递增为在我看来,我认为你总是希望“以前的值”是最新的更自然。有 2 个按钮都应该增加一个值,尽管方式不同 - 我认为如果同时单击它们,根据时间的不同,将其称为错误是合理的。

但这当然取决于每个单独的组件或应用程序来决定。我希望我在这里所做的是解释区别是什么,并为您提供选择可能最好的基础。但我相信 90+% 的情况下,如果您可以选择使用函数参数(“方法 1”),它会更好,除非您知道它不是。

【讨论】:

    【解决方案2】:

    第一种方法 setIsChanged((prevState) =&gt; !prevState)

    以确保您始终拥有更改之前的最后一个状态。

    【讨论】:

    • 根据问题和您的回答中的一些 cmets,我知道这是异步操作,但我也假设没有遗漏任何状态,否则将是一团糟。如果是这样的话,我无法理解第一种方法到底有多好。
    • 抱歉迟到了,您可以同时使用这两种方法,它们都可以正常工作。但是如果我们使用了多个状态并且它们的值会根据某些条件发生变化,那么我们最终会不知道它最终的最后一个状态。具体到我们对特定状态的最后一个状态,然后 react 为我们提供了 setState 中的 prevState,所以我们可以说嗨,请采用该状态达到的最后一个状态并将其更新为新状态。您可能会说最大的区别是什么,但有时获取最后一个状态或知道它是什么会有所帮助。
    【解决方案3】:

    简单的答案是这样的:

    setIsChanged((prevState) =&gt; !prevState) 在这种情况下,useState 钩子提供的 setter setIsChanged 正在传递它自己的内部引用值,因此它总是会更新,尽管 useState 存在整个异步问题。

    简而言之,您将内部状态值与prevState 一起使用。

    setIsChanged(!isChanged) 在这种情况下,您使用的是 useState 挂钩为您的 COMPONENT 提供的值,而不是内部引用。在这种情况下,您正在组件中本地处理数据,由于组件的异步工作方式,这些数据可能会过时。

    在大多数情况下,第二种方法效果很好,并且是迄今为止最常见的。如果您同时更新多个位置的状态或感觉它可能会不同步,您只会使用第二个。

    你可以阅读更多关于here

    【讨论】:

      【解决方案4】:

      状态更新是异步的,这意味着它们不会立即更新。他们是预定的。如果您的状态取决于先前的状态,请使用第一种方法。

      如果您的 updatedState 不依赖于先前使用的状态,则使用第二种方法。 示例:

      1. 按钮单击以增加计数器或在状态之间切换。方法1
      2. 单击提交按钮后重置输入字段。方法2

      【讨论】:

        【解决方案5】:

        你可以试试自定义钩子

        useToggle.js

        import { useReducer } from 'react';
        
        function toggler(currentValue, newValue) {
          return typeof newValue === 'boolean' ? newValue : !currentValue;
        }
        
        function useToggle(initialValue = false) {
          return useReducer(toggler, initialValue);
        }
        
        export default useToggle;
        

        使用喜欢

        import useToggle from './useToggle';
        
        const App = () => {
          const [isShown, toggle] = useToggle();
          return <button>{isShown ? 'show' : 'hide'}</button>;
        };
        export default App;
        

        【讨论】:

        • 这甚至没有尝试回答 OP 提出的问题。它甚至没有提到useState 钩子!
        猜你喜欢
        • 1970-01-01
        • 2020-11-30
        • 1970-01-01
        • 2020-05-12
        • 1970-01-01
        • 2020-04-29
        • 2020-05-23
        • 2020-04-05
        • 2020-11-29
        相关资源
        最近更新 更多