使用useState钩子在React中设置布尔状态的更好方法



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

const [isChanged, setIsChanged] = useState<boolean>(false)

const onClick = () => {
setIsChanged((prevState) => !prevState)  // Approach 1
setIsChanged(!isChanged)  // Approach 2
}

因为在代码中,一个简单的例子往往会描绘出千言万语,这里有一个简单地CodeSandbox演示来说明差异,以及为什么,如果你想要基于更新点的状态值进行更新;更新器函数";(方法1)最好:

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

这是一个独立的代码片段:

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

这里我们有一个简单的数字"0";计数";显示在标记中的状态,以及3个不同的按钮,所有按钮都会递增。

上面的那个只是直接递增——我碰巧在这里使用了函数形式("方法1"),因为我更喜欢这种风格,原因有望变得清楚,但正如我的评论所说,这在这里并不重要。

下面两个使用你在问题中概述的两种不同的方法,并在延迟后进行。我在这里用setTimeout做这件事只是为了简单-虽然这不是特别现实,但在实际应用中,类似的效果通常会出现在动作调用API端点的应用中(尽管人们希望这通常不会花3秒,但在更快的请求中总是可以观察到同样的问题-我只是放慢了速度,以便更容易触发错误)。

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

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

你应该看到行为上的明显差异:

  • 与";方法1";(右边的按钮,我在这里称之为"好"),超时结束后计数第二次递增
  • 用";方法2";(左边的按钮,我称之为"bug"),无论你等待多长时间,中间点击顶部按钮产生的值都不会进一步增加

(如果你快速点击底部按钮多次,然后点击顶部按钮一次,你会更明显地看到这一点。为了获得更违反直觉的效果,试着按下底部按钮一次或多次,然后在3秒的时间间隔内多次点击顶部按钮。)

为什么会发生这种情况?嗯;童车;发生行为是因为setTimeout内部的函数是外部变量count的闭包,该外部变量在全组件函数的范围内。这意味着,当以count + 1为参数调用它时,它会将计数更新为1,而不是定义函数时的计数。假设您从第一次加载计数为0的组件开始执行上述顺序,然后发生的更详细的顺序是:

  • 点击底部按钮将在3秒钟后进行回调。由于此时的count等于0,因此其自变量count + 1等于1
  • 单击顶部按钮可重新发送组件,计数现在等于1
  • 在第一步设置的回调稍后会触发,并将计数设置为1。这不会引起任何明显的机会,因为计数已经是1了。(如果你尝试多次点击顶部按钮,现在它显示2或更多,这实际上会使计数器递减,因为正如我所解释的,它总是设置为1。)

如果您对JS闭包有一点了解,您可能会想知道为什么在闭包中访问的count仍然是0。以前不是更新为1吗?不,事实并非如此,这可能有点违反直觉。注意到count是如何用const声明的吗?没错,它从来没有真正改变过。UI更新的原因是setCount导致React重新提交您的组件,这意味着与该组件对应的整个外部函数将被再次调用。这将设置一个全新的环境,其中包含一个新的count变量。React的内部确保useState调用现在为当前计数返回1,因此当前计数是新的"0"中的值;实例";但从3秒钟后放入事件队列中激发的函数的角度来看,这是无关紧要的。就它而言,count变量-不再在范围内,而是";记住";在该回调中,所有封闭变量都是-从未从0更改过。等于1的计数完全在不同的范围内,并且第一次回调永远无法访问。

函数自变量是如何形成的;方法1"-绕过这个?非常容易。它根本不包含任何闭包——该函数内部的变量与外部的count无关,为了准确性和消除与外部count的歧义,我在这里称之为oldCount。它是React本身将在内部调用的函数的参数。当React调用函数时,它总是提供";最新的";状态值。所以你不必担心";陈旧的闭包";或者类似的东西——你说的是";不管最近的值是什么都将计数更新为比该值多一个";,React会处理剩下的。

我称之为方法2";被窃听";因为我认为,如果你点击了一个设置为进行增量的按钮,那么在超时后预计会发生增量是合理的。但这并不总是你想要的。如果你真的希望更新基于第一次点击按钮时的值,那么你当然会更喜欢方法2,而方法1似乎有问题。从某种意义上说,情况往往如此。我强烈建议阅读核心React开发人员之一Dan Abramov的这篇文章,该文章解释了类组件和函数之间的一个关键区别,该区别基于许多关于闭包的相同论点,通常情况下,您希望事件处理程序引用呈现时的值,而不是当它们在API请求或超时后实际激发时。

但这篇帖子与";方法1";状态更新函数的形式,这在文章中甚至没有提到。这是因为它与给定的例子无关-没有(明智的)方法来重写这些例子来使用它。但是当你确实想要根据其先前的值更新状态值时,就像在OP例子中否定布尔值或在我的例子中增加计数器一样,我认为你总是想要";先前值";最新。有两个按钮都应该增加一个值,尽管方式不同——如果根据时间的不同,同时点击两个按钮可能总共只增加一次,我认为称之为bug是合理的。

但这当然取决于每个单独的组件或应用程序。我希望我在这里所做的是解释区别,并给你一个选择哪种可能是最好的基础。但我相信,在90%以上的情况下,如果你可以选择使用函数参数("方法1"),它会更好,除非你知道不是。

第一种方法setIsChanged((prevState) => !prevState)

以确保在更改前始终具有最后一个状态。

简单的答案是:

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

简而言之,您使用的是prevState的内部状态值。

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

在大多数情况下,第二种方法效果很好,是迄今为止最常见的方法。只有当你同时在多个地方更新状态,或者觉得状态可能不同步时,你才会使用第二个。

你可以在这里阅读更多关于

状态更新是异步的,这意味着它们不会立即更新。他们已经安排好了。如果你的状态取决于之前的状态,那么使用第一种方法。

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

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

useState是异步的,所以有时这种方式不适用于

setEditing(!prevState) 

首先,我们需要以前的值,我认为这种方式是最好的

setEditing((prevState) => !prevState)  

您可以尝试自定义挂钩

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;

最新更新