如何管理 React 类组件上的状态

作者选择了 Creative Commons以作为 Write for Donations计划的一部分获得捐赠。

介绍

React, state 是指一个结构,它跟踪应用程序中的数据随着时间的推移而发生变化。在 React 中,管理状态是至关重要的技能,因为它允许您创建交互式组件和动态 Web 应用程序。 状态用于从跟踪表单输入到从 API 捕捉动态数据的一切。

正如本教程所写的那样,官方 React 文档鼓励开发人员在编写新代码时采用 React Hooks来管理状态(https://andsky.com/tech/tutorials/how-to-create-custom-components-in-react#step-4-%E2%80%94-building-a-functional-component)而不是使用 基于类的组件。虽然使用 React Hooks 被认为是一种更现代的做法,但重要的是了解如何管理基于类的组件的状态。学习国家管理背后的概念将帮助您在现有代码库中导航和解决基于类的状态管理问题,并帮助您决定基于类的国家管理是否更合适。

本教程首先将向您展示如何使用静态值设置状态,这对于下一个状态不依赖于第一个状态的情况有用,例如设置来自超越旧值的API的数据。然后它将通过如何将状态设置为当前状态来运行,当下一个状态取决于当前状态时有用,例如转移一个值。

前提条件

步骤 1 – 创建一个空的项目

在此步骤中,您将使用 [Create React App] 创建一个新项目(https://github.com/facebook/create-react-app)。然后您将删除在启动项目时安装的样本项目和相关文件。最后,您将创建一个简单的文件结构来组织您的组件。

要开始,创建一个新项目. 在终端中,运行以下脚本以使用create-react-app安装新项目:

1npx create-react-app state-class-tutorial

项目完成后,更改到目录:

1cd state-class-tutorial

在新的终端卡或窗口中,使用 Create React App start script启动项目。

1npm start

如果项目未在浏览器窗口中打开,您可以使用 http://localhost:3000/ 打开它。 如果您正在从远程服务器运行,则地址将是 http://your_domain:3000

您的浏览器将加载一个简单的 React 应用程序,作为 Create React 应用程序的一部分:

React template project

您将构建一组全新的自定义组件,因此您需要从清除一些锅炉板代码开始,以便您可以有一个空的项目。

要开始,请在文本编辑器中打开src/App.js。 这是注入到页面的根组件。 所有组件将从这里开始。 您可以找到有关App.js的更多信息,请参阅How To Set Up a React Project with Create React App(如何使用Create React App设置反应项目(LINK0))。

使用以下命令打开src/App.js:

1nano src/App.js

你會看到這樣的檔案:

 1[label state-class-tutorial/src/App.js]
 2import React from 'react';
 3import logo from './logo.svg';
 4import './App.css';
 5
 6function App() {
 7  return (
 8    <div className="App">
 9      <header className="App-header">
10        <img src={logo} className="App-logo" alt="logo" />
11        <p>
12          Edit <code>src/App.js</code> and save to reload.
13        </p>
14        <a
15          className="App-link"
16          href="https://reactjs.org"
17          target="_blank"
18          rel="noopener noreferrer"
19        >
20          Learn React
21        </a>
22      </header>
23    </div>
24  );
25}
26
27export default App;

删除./logo.svg导入标签行,然后更换返回声明中的所有内容,以返回一组空标签:<></>

 1[label state-class-tutorial/src/App.js]
 2
 3import React from 'react';
 4import './App.css';
 5
 6function App() {
 7  return <></>;
 8}
 9
10export default App;

保存和退出文本编辑器。

最后,删除标志. 你不会在你的应用程序中使用它,你应该在工作时删除未使用的文件。

在终端窗口中,输入以下命令:

1rm src/logo.svg

如果你看你的浏览器,你会看到一个空白的屏幕。

blank screen in chrome

现在您已经清理了 Create React App 样本项目,创建一个简单的文件结构,这将有助于保持您的组件隔离和独立。

src目录中创建一个名为components的目录,此目录将包含您的所有自定义组件。

1mkdir src/components

每个组件将有自己的目录来存储组件文件,以及风格,图像和测试。

创建一个App目录:

1mkdir src/components/App

将所有应用文件移动到该目录中。 使用 wildcard,以选择任何开始于应用的文件,无论文件的扩展,然后使用mv命令将其放入新目录:

1mv src/App.* src/components/App

接下来,更新index.js中的相对导入路径,这是启动整个过程的根组件:

1nano src/index.js

导入声明需要指向应用程序目录中的App.js文件,所以请做出以下突出更改:

 1[label state-class-tutorial/src/index.js]
 2import React from 'react';
 3import ReactDOM from 'react-dom';
 4import './index.css';
 5import App from './components/App/App';
 6import * as serviceWorker from './serviceWorker';
 7
 8ReactDOM.render(
 9  <React.StrictMode>
10    <App />
11  </React.StrictMode>,
12  document.getElementById('root')
13);
14
15// If you want your app to work offline and load faster, you can change
16// unregister() to register() below. Note this comes with some pitfalls.
17// Learn more about service workers: https://bit.ly/CRA-PWA
18serviceWorker.unregister();

保存和退出文件。

现在项目已经设置,您可以创建您的第一个组件。

步骤 2 – 在组件中使用状态

在此步骤中,您将将组件的初始状态设置为其类,并参考该状态以显示一个值。

构建组件

首先,创建一个产品目录:

1mkdir src/components/Product

接下来,在该目录中打开Product.js:

1nano src/components/Product/Product.js

开始创建一个没有状态的组件. 该组件将有两个部分:篮子,其中有项目数量和总价格,以及产品,其中有一个按钮添加和删除一个项目。

将以下代码添加到Product.js:

 1[label state-class-tutorial/src/components/Product/Product.js]
 2import React, { Component } from 'react';
 3import './Product.css';
 4
 5export default class Product extends Component {
 6  render() {
 7    return(
 8      <div className="wrapper">
 9        <div>
10          Shopping Cart: 0 total items.
11        </div>
12        <div>Total: 0</div>
13
14        <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
15        <button>Add</button> <button>Remove</button>
16      </div>
17    )
18  }
19}

您还包括了一对具有 JSX类名称的div元素,因此您可以添加一些基本的风格。

保存并关闭文件,然后打开Product.css:

1nano src/components/Product/Product.css

给一些轻松的风格来增加文本和情感符的字体大小:

 1[label state-class-tutorial/src/components/Product/Product.css]
 2.product span {
 3    font-size: 100px;
 4}
 5
 6.wrapper {
 7    padding: 20px;
 8    font-size: 20px;
 9}
10
11.wrapper button {
12    font-size: 20px;
13    background: none;
14}

Emoji 将需要比文本大得多的字体大小,因为它在本示例中作为产品图像。

保存并关闭文件。

现在,在应用程序组件中渲染产品组件,这样你就可以在浏览器中看到结果。

1nano src/components/App/App.js

您也可以删除CSS导入,因为您不会在本教程中使用它:

1[label state-class-tutorial/src/components/App/App.js]
2import React from 'react';
3import Product from '../Product/Product';
4
5function App() {
6  return <Product />
7}
8
9export default App;

当你这样做时,浏览器会更新,你会看到产品组件。

Product Page

在一类组件上设置初始状态

您的组件值中有两种值将在显示中发生变化:项目总数和总成本,而不是硬编码它们,在此步骤中,您将它们移动到一个名为状态对象

React 类的状态是控制页面渲染的特殊属性。当您更改状态时,React 知道该组件已过时,并且会自动重新渲染。当组件重新渲染时,它会修改渲染的输出,以便在状态中包含最新的信息。

打开Product.js:

1nano src/components/Product/Product.js

将一个名为状态的属性添加到产品类中,然后将两个值添加到状态对象中:总数将是一个(https://andsky.com/tech/tutorials/understanding-arrays-in-javascript),因为它最终可能包含许多项目。

 1[label state-class-tutorial/src/components/Product/Product.js]
 2
 3import React, { Component } from 'react';
 4import './Product.css';
 5
 6export default class Product extends Component {
 7
 8  state = {
 9    cart: [],
10    total: 0
11  }
12
13  render() {
14    return(
15      <div className="wrapper">
16        <div>
17          Shopping Cart: {this.state.cart.length} total items.
18        </div>
19        <div>Total {this.state.total}</div>
20
21        <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
22        <button>Add</button> <button>Remove</button>
23      </div>
24    )
25  }
26}

请注意,在这两种情况下,因为您正在引用JavaScript在您的JSX中,您需要将代码包装在弯曲的框中。

当您这样做时,浏览器将更新,您将看到与以前相同的页面。

Product Page

国家属性是一个标准类属性,这意味着它可以通过其他方法访问,而不仅仅是交付方法。

接下来,而不是将价格显示为静态值,则使用 toLocaleString 方法将其转换为字符串,这将将数字转换为与浏览器区域中的数字显示方式相匹配的字符串。

创建一种名为getTotal()的方法,将状态转换为使用货币选项数组的本地化字符串。

 1[label state-class-tutorial/src/components/Product/Product.js]
 2import React, { Component } from 'react';
 3import './Product.css';
 4
 5export default class Product extends Component {
 6
 7  state = {
 8    cart: [],
 9    total: 0
10  }
11
12  currencyOptions = {
13    minimumFractionDigits: 2,
14    maximumFractionDigits: 2,
15  }
16
17  getTotal = () => {
18    return this.state.total.toLocaleString(undefined, this.currencyOptions)
19  }
20
21  render() {
22    return(
23      <div className="wrapper">
24        <div>
25          Shopping Cart: {this.state.cart.length} total items.
26        </div>
27        <div>Total {this.getTotal()}</div>
28
29        <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
30        <button>Add</button> <button>Remove</button>
31      </div>
32    )
33  }
34}

由于是商品的价格,你正在通过货币选项,为你的设置最大和最低的十进制位。 请注意,这是一个单独的属性。 通常,初学者 React 开发人员会将此类信息放在状态对象中,但最好只将信息添加到你希望改变的状态

另一个重要的变化是创建getTotal()方法,将箭头函数(https://andsky.com/tech/tutorials/getting-started-with-es6-arrow-functions-in-javascript)分配给一个类属性,而不使用箭头函数,这种方法会创建一个新的(https://www.digitalocean.com/community/conceptual_articles/understanding-this-bind-call-and-apply-in-javascript),这会干扰当前的(This的)束缚,并在我们的代码中引入一个错误。

当您这样做时,页面将更新,您将看到转换为十进制的值。

Price converted to decimal

您现在已将状态添加到组件中,并将其引用到您的类中;您还访问了渲染方法和其他类方法中的值;接下来,您将创建方法来更新状态并显示动态值。

步骤 3 – 从静态值中设置状态

到目前为止,您已经为该组件创建了一个基本状态,并在您的函数和您的 JSX 代码中引用了该状态。 在此步骤中,您将更新您的产品页面,以便在按钮点击时更改状态

要更新状态,React 开发人员使用一个名为setState的特殊方法,该方法是从基础Component类继承的。setState方法可以将一个对象或函数作为第一个参数。

如果你的用户点击了 ** 添加**,那么程序会将该项目添加到并更新到总数。如果他们点击了 ** 删除**,则会将篮子重置为空数组,而总数将重置为0

打开Product.js:

1nano src/components/Product/Product.js

在组件中,创建一个名为添加的新方法,然后将该方法传递到添加按钮的onClick支架:

 1[label state-class-tutorial/src/components/Product/Product.js]
 2import React, { Component } from 'react';
 3import './Product.css';
 4
 5export default class Product extends Component {
 6
 7  state = {
 8    cart: [],
 9    total: 0
10  }
11
12  add = () => {
13    this.setState({
14      cart: ['ice cream'],
15      total: 5
16    })
17  }
18
19  currencyOptions = {
20    minimumFractionDigits: 2,
21    maximumFractionDigits: 2,
22  }
23
24  getTotal = () => {
25    return this.state.total.toLocaleString(undefined, this.currencyOptions)
26  }
27
28  render() {
29    return(
30      <div className="wrapper">
31        <div>
32          Shopping Cart: {this.state.cart.length} total items.
33        </div>
34        <div>Total {this.getTotal()}</div>
35
36        <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
37        <button onClick={this.add}>Add</button>
38        <button>Remove</button>
39      </div>
40    )
41  }
42}

添加方法中,您打电话给setState方法,然后传递一个包含更新的对象,其中包含一个冰淇淋5的更新价格。 请注意,您再次使用箭头函数创建添加方法。

例如,如果您以此方式创建了添加函数:

 1export default class Product extends Component {
 2...
 3  add() {
 4    this.setState({
 5      cart: ['ice cream'],
 6      total: 5
 7    })
 8  }
 9...
10}

用户在点击添加按钮时会收到错误。

Context Error

使用箭头函数确保您有正确的背景,以避免此错误。

当你这样做时,浏览器将重新加载,当你点击添加按钮时,篮子将更新当前的金额。

Click on the button and see state updated

使用添加方法,您通过了状态对象的两个属性:总数。但是,您不需要总是通过一个完整的对象。

要查看 React 如何处理较小的对象,请创建一个名为删除的新函数. 将仅包含卡片的新对象传输到空数组中,然后将该方法添加到 删除按钮的onClick属性:

 1[label state-class-tutorial/src/components/Product/Product.js]
 2import React, { Component } from 'react';
 3import './Product.css';
 4
 5export default class Product extends Component {
 6
 7  ...
 8  remove = () => {
 9    this.setState({
10      cart: []
11    })
12  }
13
14  render() {
15    return(
16      <div className="wrapper">
17        <div>
18          Shopping Cart: {this.state.cart.length} total items.
19        </div>
20        <div>Total {this.getTotal()}</div>
21
22        <div className="product"><span role="img" aria-label="ice cream">🍦</span></div>
23        <button onClick={this.add}>Add</button>
24        <button onClick={this.remove}>Remove</button>
25      </div>
26    )
27  }
28}

保存文件. 当浏览器更新时,点击 ** 添加** 和 ** 删除** 按钮. 您将看到篮子更新,但不是价格。 更新期间保留状态值. 此值仅保留为示例目的; 使用此应用程序,您希望更新状态对象的两种属性。

此步骤中的变化是静态的. 你确切地知道值将是什么,并且不需要从状态重新计算。但是,如果产品页面有许多产品,并且你想能够将它们添加多次,通过静态对象不会提供最新的状态引用的保证,即使你的对象使用了this.state值. 在这种情况下,你可以使用函数。

在下一步中,您将使用参考当前状态的函数更新状态

步骤 4 – 使用当前状态设置状态

有很多时候你需要引用以前的状态来更新当前状态,例如更新数组、添加数字或修改对象. 要尽可能准确,你需要引用最新的状态对象。

使用函数设置状态的另一个好处是可靠性增加。为了提高性能,React 可能会批量调用setState,这意味着this.state.value可能不完全可靠。例如,如果您在多个位置快速更新状态,则可能有一个值过时。

要展示这种形式的状态管理,请在产品页面中添加一些更多项目,首先打开Product.js文件:

1nano src/components/Product/Product.js

接下来,为不同的产品创建一个对象阵列. 阵列将包含产品情感符号、名称和价格. 然后在阵列上旋转以显示每个产品用 ** 添加** 和 ** 删除** 按钮:

 1[label state-class-tutorial/src/components/Product/Product.js]
 2import React, { Component } from 'react';
 3import './Product.css';
 4
 5const products = [
 6  {
 7    emoji: '🍦',
 8    name: 'ice cream',
 9    price: 5
10  },
11  {
12    emoji: '🍩',
13    name: 'donuts',
14    price: 2.5,
15  },
16  {
17    emoji: '🍉',
18    name: 'watermelon',
19    price: 4
20  }
21];
22
23export default class Product extends Component {
24
25  ...
26
27  render() {
28    return(
29      <div className="wrapper">
30        <div>
31          Shopping Cart: {this.state.cart.length} total items.
32        </div>
33        <div>Total {this.getTotal()}</div>
34        <div>
35          {products.map(product => (
36            <div key={product.name}>
37              <div className="product">
38                <span role="img" aria-label={product.name}>{product.emoji}</span>
39              </div>
40              <button onClick={this.add}>Add</button>
41              <button onClick={this.remove}>Remove</button>
42            </div>
43          ))}
44        </div>
45      </div>
46    )
47  }
48}

在此代码中,您正在使用 map() 数组方法来旋转产品数组,并返回将显示浏览器中的每个元素的 JSX。

当浏览器重新加载时,您将看到一个更新的产品列表:

Product list

现在你需要更新你的方法。 首先,更改add()方法以将产品作为参数。 然后,而不是将一个对象传输到setState(),传输一个函数,将状态作为参数,并返回一个对象,该对象具有新产品更新的和新价格更新的总数:

 1[label state-class-tutorial/src/components/Product/Product.js]
 2import React, { Component } from 'react';
 3import './Product.css';
 4
 5...
 6
 7export default class Product extends Component {
 8
 9  state = {
10    cart: [],
11    total: 0
12  }
13
14  add = (product) => {
15    this.setState(state => ({
16      cart: [...state.cart, product.name],
17      total: state.total + product.price
18    }))
19  }
20
21  currencyOptions = {
22    minimumFractionDigits: 2,
23    maximumFractionDigits: 2,
24  }
25
26  getTotal = () => {
27    return this.state.total.toLocaleString(undefined, this.currencyOptions)
28  }
29
30  remove = () => {
31    this.setState({
32      cart: []
33    })
34  }
35
36  render() {
37    return(
38      <div className="wrapper">
39        <div>
40          Shopping Cart: {this.state.cart.length} total items.
41        </div>
42        <div>Total {this.getTotal()}</div>
43
44        <div>
45          {products.map(product => (
46            <div key={product.name}>
47              <div className="product">
48                <span role="img" aria-label={product.name}>{product.emoji}</span>
49              </div>
50              <button onClick={() => this.add(product)}>Add</button>
51              <button onClick={this.remove}>Remove</button>
52            </div>
53          ))}
54        </div>
55      </div>
56    )
57  }
58}

在匿名函数中,您将它转移到setState(),请确保您引用参数state,而不是组件的状态this.state

注意不要直接改变状态;相反,当您将新值添加到卡片中时,您可以使用当前值上的 扩散语法并在末尾添加新值来添加新的产品状态

最后,通过更改onClick()口号来更新调用到this.add以使用一个匿名函数,该函数与相应的产品一起调用this.add()

当你这样做时,浏览器将重新加载,你将能够添加多个产品。

Adding products

接下来,更新删除()方法,按照相同的步骤:转换setState以采取函数,更新值而不发生突变,并更新onChange()预告片:

 1[label state-class-tutorial/src/components/Product/Product.js]
 2import React, { Component } from 'react';
 3import './Product.css';
 4
 5...
 6
 7export default class Product extends Component {
 8
 9...
10
11  remove = (product) => {
12    this.setState(state => {
13      const cart = [...state.cart];
14      cart.splice(cart.indexOf(product.name))
15      return ({
16        cart,
17        total: state.total - product.price
18      })
19    })
20  }
21
22  render() {
23    return(
24      <div className="wrapper">
25        <div>
26          Shopping Cart: {this.state.cart.length} total items.
27        </div>
28        <div>Total {this.getTotal()}</div>
29        <div>
30          {products.map(product => (
31            <div key={product.name}>
32              <div className="product">
33                <span role="img" aria-label={product.name}>{product.emoji}</span>
34              </div>
35              <button onClick={() => this.add(product)}>Add</button>
36              <button onClick={() => this.remove(product)}>Remove</button>
37            </div>
38          ))}
39        </div>
40      </div>
41    )
42  }
43}

要避免突变状态对象,您必须首先使用散布操作员来复制它,然后您可以 splice从副本中删除您想要的项目,并在新对象中返回副本。

当你这样做时,浏览器将更新,你将能够添加和删除项目:

Remove items

在这个应用程序中仍然存在一个错误:在删除方法中,用户可以从中扣除,即使该项目不在篮子中。

您可以通过在减去之前检查一个项目的存在来修复这个错误,但一个更简单的方法是将状态对象保持小,只保留对产品的引用,而不是将产品和总成本的引用分开。

重构组件,以便add()方法添加整个对象,remove()方法删除整个对象,而getTotal方法使用cart:

 1[label state-class-tutorial/src/components/Product/Product.js]
 2import React, { Component } from 'react';
 3import './Product.css';
 4
 5...
 6
 7export default class Product extends Component {
 8
 9  state = {
10    cart: [],
11  }
12
13  add = (product) => {
14    this.setState(state => ({
15      cart: [...state.cart, product],
16    }))
17  }
18
19  currencyOptions = {
20    minimumFractionDigits: 2,
21    maximumFractionDigits: 2,
22  }
23
24  getTotal = () => {
25    const total = this.state.cart.reduce((totalCost, item) => totalCost + item.price, 0);
26    return total.toLocaleString(undefined, this.currencyOptions)
27  }
28
29  remove = (product) => {
30    this.setState(state => {
31      const cart = [...state.cart];
32      const productIndex = cart.findIndex(p => p.name === product.name);
33      if(productIndex < 0) {
34        return;
35      }
36      cart.splice(productIndex, 1)
37      return ({
38        cart
39      })
40    })
41  }
42
43  render() {
44    ...
45  }
46}

删除()方法中,你会找到findByIndex产品索引。如果索引不存在,你会得到一个-1。在这种情况下,你会使用条件声明(https://andsky.com/tech/tutorials/how-to-write-conditional-statements-in-javascript)返回任何东西。通过返回任何东西,React 会知道状态没有改变,不会触发重现。如果状态返回或一个空的对象,它仍然会触发重现。

当使用splice()方法时,您现在将1作为第二个参数,这将删除一个值并保留其余的值。

最后,您使用 reduce())数组方法计算总数

当你这样做时,浏览器将更新,你将有你的最后的:

Add and remove

您通过的setState函数可以具有当前代理的额外参数,如果您有需要引用当前代理的状态,则可以有帮助。

在此步骤中,您了解如何根据当前状态更新新状态. 您将函数传输到setState函数并计算新的值,而无需突变当前状态。

结论

在本教程中,您已经开发了一个基于类的组件,具有动态状态,您已静态更新并使用当前状态. 您现在有工具来创建响应用户和动态信息的复杂项目。

React 确实有方法可以使用 Hooks 来管理状态,但如果您需要使用必须基于类的组件,例如使用componentDidCatch方法的组件,则了解如何在组件上使用状态是有用的。

管理状态是几乎所有组件的关键,并且是创建交互式应用程序所必需的。使用这种知识,您可以重建许多常见的Web组件,如滑板,调节符,表单等。

如果您想查看更多 React 教程,请查看我们的 React 主题页面,或返回 如何在 React.js 系列中编码页面

Published At
Categories with 技术
comments powered by Disqus