理解 React `setState`

Avatar of Kingsley Silas
Kingsley Silas

DigitalOcean 提供适合您旅程各个阶段的云产品。立即开始使用 $200 免费赠送额度!

React 组件可以并且经常具有 **状态**。状态可以是任何东西,但可以考虑诸如用户是否已登录以及根据哪个帐户处于活动状态来显示正确的用户名之类的事情。或者是一系列博客文章。或者模态窗口是否打开以及其中哪个选项卡处于活动状态。

具有状态的 React 组件根据该状态呈现 UI。当组件的状态发生变化时,组件 UI 也会发生变化。

这使得理解 何时 以及 如何 更改组件状态变得很重要。在本教程结束时,您应该了解 setState 的工作原理,并能够避免我们许多人在学习 React 时遇到的常见陷阱。

`setState()` 的工作原理

`setState()` 是在初始状态设置后更新状态的唯一合法方法。假设我们有一个搜索组件,并且想要显示用户提交的搜索词。

以下是设置

import React, { Component } from 'react'

class Search extends Component {
  constructor(props) {
    super(props)

    state = {
      searchTerm: ''
    }
  }
}

我们传递一个空字符串作为值,为了更新 `searchTerm` 的状态,我们必须调用 `setState()`。

setState({ searchTerm: event.target.value })

在这里,我们向 `setState()` 传递一个对象。该对象包含我们要更新的状态部分,在本例中是 `searchTerm` 的值。React 获取此值并将其合并到需要它的对象中。这有点像 `Search` 组件询问它应该使用什么作为 `searchTerm` 的值,而 `setState()` 则用答案进行回应。

这基本上启动了 React 称为 协调 的过程。协调过程是 React 更新 DOM 的方式,通过根据状态的变化对组件进行更改来实现。当触发对 `setState()` 的请求时,React 会创建一个包含组件中反应式元素(以及更新后的状态)的新树。此树用于通过将它与前一棵树的元素进行比较来确定如何更改 `Search` 组件的 UI 以响应状态变化。React 知道要实施哪些更改,并且只会更新 DOM 中必要的部分。这就是 React 速度快的原因。

听起来很复杂,但总结一下流程

  • 我们有一个搜索组件,它显示一个搜索词
  • 该搜索词目前为空
  • 用户提交一个搜索词
  • 该词被 `setState` 捕获并存储为一个值
  • 协调过程发生,React 发现值的更改
  • React 指示搜索组件更新值,并合并搜索词

协调过程不一定更改整个树,除非树的根部发生更改,例如这种情况

// old
<div>
  <Search />
</div>

// new
<span>
  <Search />
</span>

所有 `<div>` 标记都变为 `<span>` 标记,结果整个组件树将被更新。

经验法则是永远不要直接修改状态。始终使用 `setState()` 来更改状态。直接修改状态,例如下面的代码片段,不会导致组件重新渲染。

// do not do this
this.state = {
  searchTerm: event.target.value
}

向 `setState()` 传递函数

为了进一步说明这个想法,让我们创建一个简单的计数器,它在点击时增量和减量。

查看 CodePen 上 Kingsley Silas Chijioke (@kinsomicrote) 的 setState Pen

让我们注册组件并定义 UI 的标记

class App extends React.Component {

state = { count: 0 }

handleIncrement = () => {
  this.setState({ count: this.state.count + 1 })
}

handleDecrement = () => {
  this.setState({ count: this.state.count - 1 })
}
  render() {
    return (
      <div>
        <div>
          {this.state.count}
        </div>
        <button onClick={this.handleIncrement}>Increment by 1</button>
        <button onClick={this.handleDecrement}>Decrement by 1</button>
      </div>
    )
  }
}

在这一点上,计数器只是在每次点击时将计数增量或减量 1。

但如果我们想要增量或减量 3 呢?我们可以尝试在 `handleDecrement` 和 `handleIncrement` 函数中调用 `setState()` 三次,如下所示

handleIncrement = () => {
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
}

handleDecrement = () => {
  this.setState({ count: this.state.count - 1 })
  this.setState({ count: this.state.count - 1 })
  this.setState({ count: this.state.count - 1 })
}

如果您在家中一起编码,您可能会惊讶地发现 **这不起作用。**

上面的代码片段等效于

Object.assign(  
  {},
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
)

`Object.assign()` 用于将数据从源对象复制到目标对象。如果从源对象复制到目标对象的数据具有相同的键,就像我们的示例一样,最后一个对象将获胜。以下是 `Object.assign()` 工作原理的更简单版本;

let count = 3

const object = Object.assign({}, 
  {count: count + 1}, 
  {count: count + 2}, 
  {count: count + 3}
);

console.log(object);
// output: Object { count: 6 }

因此,调用并非发生三次,而只发生一次。这可以通过向 `setState()` 传递函数来解决。就像您向 `setState()` 传递对象一样,您也可以传递函数,这是摆脱上述情况的方法。

如果我们将 `handleIncrement` 函数编辑为如下所示

handleIncrement = () => {
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
}

…我们现在可以通过一次点击将计数增量三次。

在这种情况下,React 不会合并,而是按其创建顺序将函数调用排队,并在完成后更新整个状态。这将计数的状态更新为 3 而不是 1。

使用更新器访问先前状态

在构建 React 应用程序时,有时您需要根据组件的先前状态计算状态。您不能总是相信 `this.state` 在调用 `setState()` 后立即保存正确的状态,因为它始终等于屏幕上渲染的状态。

让我们回到我们的计数器示例,看看它是如何工作的。假设我们有一个函数,它将我们的计数减量 1。这个函数看起来像这样

changeCount = () => {
  this.setState({ count: this.state.count - 1})
}

我们想要的是能够减量 3。`changeCount()` 函数在处理点击事件的函数中被调用三次,如下所示。

handleDecrement = () => {
  this.changeCount()
  this.changeCount()
  this.changeCount()
}

每次点击减量按钮时,计数将减量 1 而不是 3。这是因为 `this.state.count` 直到组件重新渲染后才会更新。解决方案是使用更新器。更新器允许您访问当前状态并立即使用它来更新其他项目。因此,`changeCount()` 函数将如下所示。

changeCount = () => {
  this.setState((prevState) => {
    return { count: prevState.count - 1}
  })
}

现在我们不再依赖于 `this.state` 的结果。`count` 的状态彼此建立,因此我们能够访问正确的状态,该状态随着每次对 `changeCount()` 的调用而发生变化。

`setState()` 应该被视为异步的——换句话说,不要总是期望在调用 `setState()` 后状态已经改变。

总结

在使用 `setState()` 时,以下是你应该了解的主要内容

  • 对组件状态的更新应使用 `setState()` 进行
  • 您可以向 `setState()` 传递对象或函数
  • 当您需要多次更新状态时,传递函数
  • 不要依赖于在调用 `setState()` 后立即执行的 this.state,而是使用更新器函数。