作为 Write for DOnations 计划的一部分,作者选择了 Creative Commons 来接受捐赠。
介绍
在 React 中,state 指的是一种跟踪应用程序中数据随时间变化的结构。 管理状态是 React 中的一项关键技能,因为它允许您制作交互式组件和动态 Web 应用程序。 状态用于从跟踪表单输入到从 API 捕获动态数据的所有内容。 在本教程中,您将通过一个示例在基于类的 组件 上管理 状态 。
在撰写本教程时,官方的 React 文档 鼓励开发人员在编写新代码时采用 React Hooks 使用 功能组件 来管理状态,而不是使用基于类的组件。 尽管 React Hooks 的使用被认为是一种更现代的实践,但了解如何管理基于类的组件的状态也很重要。 学习状态管理背后的概念将帮助您在现有代码库中导航和解决基于类的状态管理问题,并帮助您确定何时更适合基于类的状态管理。 还有一个名为 componentDidCatch 的基于类的方法在 Hooks 中不可用,需要使用类方法设置状态。
本教程将首先向您展示如何使用静态值设置状态,这对于下一个状态不依赖于第一个状态的情况很有用,例如从覆盖旧值的 API 设置数据。 然后会运行如何将一个状态设置为当前状态,这在下一个状态取决于当前状态时很有用,例如切换一个值。 要探索这些设置状态的不同方式,您将创建一个产品页面组件,您将通过从选项列表中添加购买来更新该组件。
先决条件
- 您将需要一个运行 Node.js 的开发环境; 本教程在 Node.js 版本 10.20.1 和 npm 版本 6.14.4 上进行了测试。 要在 macOS 或 Ubuntu 18.04 上安装它,请按照 如何在 macOS 上安装 Node.js 和创建本地开发环境中的步骤或 的 使用 PPA 部分安装如何在 Ubuntu 18.04 上安装 Node.js。
- 在本教程中,您将使用 Create React App 创建应用程序。 你可以在 How To Set Up a React Project with Create React App 找到使用 Create React App 安装应用程序的说明。
- 您还需要 JavaScript 的基本知识,您可以在 如何在 JavaScript 中找到这些知识,以及 HTML 和 CSS 的基本知识。 HTML 和 CSS 的一个很好的资源是 Mozilla 开发者网络 。
第 1 步——创建一个空项目
在这一步中,您将使用 Create React App 创建一个新项目。 然后,您将删除引导项目时安装的示例项目和相关文件。 最后,您将创建一个简单的文件结构来组织您的组件。 这将为您构建本教程的示例应用程序以管理基于类的组件的状态提供坚实的基础。
首先,创建一个新项目。 在您的终端中,运行以下脚本以使用 create-react-app
安装新项目:
npx create-react-app state-class-tutorial
项目完成后,进入目录:
cd state-class-tutorial
在新的终端选项卡或窗口中,使用 Create React App 启动脚本 启动项目。 浏览器将自动刷新更改,因此请在您工作时保持此脚本运行:
npm start
您将获得一个正在运行的本地服务器。 如果项目没有在浏览器窗口中打开,您可以使用 http://localhost:3000/ 打开它。 如果您从远程服务器运行它,地址将是 http://your_domain:3000
。
您的浏览器将加载一个简单的 React 应用程序,该应用程序包含在 Create React App 中:
您将构建一组全新的自定义组件,因此您需要首先清除一些样板代码,以便您可以拥有一个空项目。
首先,在文本编辑器中打开 src/App.js
。 这是注入页面的根组件。 所有组件都将从这里开始。 你可以在 How To Set Up a React Project with Create React App 中找到有关 App.js
的更多信息。
使用以下命令打开 src/App.js
:
nano src/App.js
你会看到一个像这样的文件:
状态类教程/src/App.js
import React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return ( <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="logo" /> <p> Edit <code>src/App.js</code> and save to reload. </p> <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer" > Learn React </a> </header> </div> ); } export default App;
删除行 import logo from './logo.svg';
。 然后替换 return
语句中的所有内容以返回一组空标签:<></>
。 这将为您提供一个不返回任何内容的有效页面。 最终代码将如下所示:
状态类教程/src/App.js
import React from 'react'; import './App.css'; function App() { return <></>; } export default App;
保存并退出文本编辑器。
最后,删除标志。 您不会在应用程序中使用它,您应该在工作时删除未使用的文件。 从长远来看,它将使您免于困惑。
在终端窗口中键入以下命令:
rm src/logo.svg
如果您查看浏览器,您将看到一个空白屏幕。
现在您已经清除了示例 Create React App 项目,创建一个简单的文件结构。 这将帮助您保持组件的独立性和独立性。
在 src
目录下创建一个名为 components
的目录。 这将包含您所有的自定义组件。
mkdir src/components
每个组件都有自己的目录来存储组件文件以及样式、图像和测试。
为 App
创建一个目录:
mkdir src/components/App
将所有 App
文件移动到该目录中。 使用通配符 *
选择以 App.
开头的任何文件,无论文件扩展名如何。 然后使用 mv
命令将它们放入新目录:
mv src/App.* src/components/App
接下来,更新index.js
中的相对导入路径,这是引导整个过程的根组件:
nano src/index.js
导入语句需要指向App
目录下的App.js
文件,所以做如下高亮修改:
状态类教程/src/index.js
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './components/App/App'; import * as serviceWorker from './serviceWorker'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
保存并退出文件。
现在项目已经建立,您可以创建您的第一个组件。
第 2 步 — 在组件中使用状态
在此步骤中,您将在其类上设置组件的初始状态并引用该状态以显示一个值。 然后,您将创建一个带有购物车的产品页面,该页面使用状态值显示购物车中的商品总数。 在该步骤结束时,您将了解保存值的不同方法以及何时应该使用状态而不是道具或静态值。
构建组件
首先为 Product
创建一个目录:
mkdir src/components/Product
接下来,在该目录中打开 Product.js
:
nano src/components/Product/Product.js
首先创建一个没有状态的组件。 该组件将包含两部分:购物车(包含商品数量和总价)和产品(包含添加和删除商品的按钮)。 目前,按钮将没有任何操作。
将以下代码添加到 Product.js
:
state-class-tutorial/src/components/Product/Product.js
import React, { Component } from 'react'; import './Product.css'; export default class Product extends Component { render() { return( <div className="wrapper"> <div> Shopping Cart: 0 total items. </div> <div>Total: 0</div> <div className="product"><span role="img" aria-label="ice cream">🍦</span></div> <button>Add</button> <button>Remove</button> </div> ) } }
您还包含了几个具有 JSX 类名称的 div
元素,因此您可以添加一些基本样式。
保存并关闭文件,然后打开 Product.css
:
nano src/components/Product/Product.css
给文本和表情符号添加一些简单的样式以增加 font-size
:
state-class-tutorial/src/components/Product/Product.css
.product span { font-size: 100px; } .wrapper { padding: 20px; font-size: 20px; } .wrapper button { font-size: 20px; background: none; }
表情符号需要比文本大得多的字体,因为它在本例中充当产品图片。 此外,您通过将 background
设置为 none
来移除按钮上的默认渐变背景。
保存并关闭文件。
现在,在 App
组件中渲染 Product
组件,以便您可以在浏览器中看到结果。 打开App.js
:
nano src/components/App/App.js
导入组件并渲染它。 您也可以删除 CSS 导入,因为您不会在本教程中使用它:
状态类教程/src/components/App/App.js
import React from 'react'; import Product from '../Product/Product'; function App() { return <Product /> } export default App;
保存并关闭文件。 当您这样做时,浏览器将刷新,您将看到 Product
组件。
在类组件上设置初始状态
您的组件值中有两个值将在您的显示中发生变化:项目总数和总成本。 在这一步中,您将把它们移动到一个名为 state
的 对象 中,而不是对它们进行硬编码。
React 类的 state
是控制页面呈现的特殊属性。 当您更改状态时,React 知道该组件已过期并会自动重新渲染。 当组件重新渲染时,它会修改渲染的输出以在 state
中包含最新信息。 在此示例中,每当您将产品添加到购物车或将其从购物车中删除时,组件都会重新呈现。 您可以将其他属性添加到 React 类,但它们不会具有触发重新渲染的相同能力。
打开Product.js
:
nano src/components/Product/Product.js
将名为 state
的属性添加到 Product
类。 然后将两个值添加到 state
对象:cart
和 total
。 cart
将是一个 array,因为它最终可能包含许多项目。 total
将是一个数字。 分配这些后,用 this.state.property
替换对值的引用:
state-class-tutorial/src/components/Product/Product.js
import React, { Component } from 'react'; import './Product.css'; export default class Product extends Component { state = { cart: [], total: 0 } render() { return( <div className="wrapper"> <div> Shopping Cart: {this.state.cart.length} total items. </div> <div>Total {this.state.total}</div> <div className="product"><span role="img" aria-label="ice cream">🍦</span></div> <button>Add</button> <button>Remove</button> </div> ) } }
请注意,在这两种情况下,由于您在 JSX 中引用 JavaScript,因此您需要将代码包装在花括号中。 您还将显示 cart
数组的 length
以获取数组中项目数的计数。
保存文件。 当您这样做时,浏览器将刷新,您将看到与以前相同的页面。
state
属性是标准类属性,这意味着它可以在其他方法中访问,而不仅仅是 render
方法。
接下来,不是将价格显示为静态值,而是使用 toLocaleString 方法将其转换为字符串,该方法会将数字转换为与数字在浏览器区域中的显示方式相匹配的字符串。
创建一个名为 getTotal()
的方法,该方法采用 state
并使用 currencyOptions
数组将其转换为本地化字符串。 然后,用方法调用替换 JSX 中对 state
的引用:
state-class-tutorial/src/components/Product/Product.js
import React, { Component } from 'react'; import './Product.css'; export default class Product extends Component { state = { cart: [], total: 0 } currencyOptions = { minimumFractionDigits: 2, maximumFractionDigits: 2, } getTotal = () => { return this.state.total.toLocaleString(undefined, this.currencyOptions) } render() { return( <div className="wrapper"> <div> Shopping Cart: {this.state.cart.length} total items. </div> <div>Total {this.getTotal()}</div> <div className="product"><span role="img" aria-label="ice cream">🍦</span></div> <button>Add</button> <button>Remove</button> </div> ) } }
由于 total
是商品的价格,因此您正在传递 currencyOptions
将 total
的最大和最小小数位设置为两位。 请注意,这被设置为单独的属性。 通常,初学者 React 开发人员会将此类信息放在 state
对象中,但最好只将您希望更改的信息添加到 state
中。 这样,随着应用程序的扩展,state
中的信息将更容易跟踪。
您所做的另一个重要更改是通过将 箭头函数 分配给类属性来创建 getTotal()
方法。 如果不使用箭头函数,此方法将创建一个新的 this 绑定,这会干扰当前的 this
绑定并在我们的代码中引入一个错误。 您将在下一步中看到更多相关信息。
保存文件。 当您这样做时,页面将刷新,您将看到转换为小数的值。
您现在已将状态添加到组件并在您的类中引用它。 您还访问了 render
方法和其他类方法中的值。 接下来,您将创建更新状态和显示动态值的方法。
第 3 步 — 从静态值设置状态
到目前为止,您已经为组件创建了一个基本状态,并且在您的函数和 JSX 代码中引用了该状态。 在此步骤中,您将更新您的产品页面以修改按钮单击时的 state
。 您将学习如何将包含更新值的新对象传递给称为 setState
的特殊方法,然后该方法将使用更新的数据设置 state
。
为了更新 state
,React 开发人员使用了一种称为 setState
的特殊方法,它继承自 Component
基类。 setState
方法可以将对象或函数作为第一个参数。 如果您有一个不需要引用 state
的静态值,最好传递一个包含新值的对象,因为它更易于阅读。 如果您需要引用当前状态,请传递一个函数以避免对过期 state
的任何引用。
首先向按钮添加事件。 如果您的用户单击 Add,则程序会将项目添加到 cart
并更新 total
。 如果他们点击 Remove,它会将购物车重置为空数组,并将 total
重置为 0
。 例如,该程序将不允许用户多次添加项目。
打开Product.js
:
nano src/components/Product/Product.js
在组件内部,创建一个名为 add
的新方法,然后将该方法传递给 Add 按钮的 onClick
属性:
state-class-tutorial/src/components/Product/Product.js
import React, { Component } from 'react'; import './Product.css'; export default class Product extends Component { state = { cart: [], total: 0 } add = () => { this.setState({ cart: ['ice cream'], total: 5 }) } currencyOptions = { minimumFractionDigits: 2, maximumFractionDigits: 2, } getTotal = () => { return this.state.total.toLocaleString(undefined, this.currencyOptions) } render() { return( <div className="wrapper"> <div> Shopping Cart: {this.state.cart.length} total items. </div> <div>Total {this.getTotal()}</div> <div className="product"><span role="img" aria-label="ice cream">🍦</span></div> <button onClick={this.add}>Add</button> <button>Remove</button> </div> ) } }
在 add
方法中,您调用 setState
方法并传递一个对象,该对象包含更新后的 cart
与单个项目 ice cream
和 [X165X 的更新价格]。 请注意,您再次使用箭头函数来创建 add
方法。 如前所述,这将确保函数在运行更新时具有正确的 this
上下文。 如果在不使用箭头函数的情况下将函数添加为方法,则在没有 binding 函数到当前上下文的情况下,setState
将不存在。
例如,如果您以这种方式创建了 add
函数:
export default class Product extends Component { ... add() { this.setState({ cart: ['ice cream'], total: 5 }) } ... }
用户单击 Add 按钮时会出现错误。
使用箭头函数可确保您有适当的上下文来避免此错误。
保存文件。 当您这样做时,浏览器将重新加载,当您单击 Add 按钮时,购物车将更新为当前金额。
使用 add
方法,您传递了 state
对象的两个属性:cart
和 total
。 但是,您并不总是需要传递一个完整的对象。 您只需要传递一个包含要更新的属性的对象,其他所有内容都将保持不变。
要了解 React 如何处理较小的对象,请创建一个名为 remove
的新函数。 传递一个仅包含 cart
和一个空数组的新对象,然后将该方法添加到 Remove 按钮的 onClick
属性中:
state-class-tutorial/src/components/Product/Product.js
import React, { Component } from 'react'; import './Product.css'; export default class Product extends Component { ... remove = () => { this.setState({ cart: [] }) } render() { return( <div className="wrapper"> <div> Shopping Cart: {this.state.cart.length} total items. </div> <div>Total {this.getTotal()}</div> <div className="product"><span role="img" aria-label="ice cream">🍦</span></div> <button onClick={this.add}>Add</button> <button onClick={this.remove}>Remove</button> </div> ) } }
保存文件。 当浏览器刷新时,单击 Add 和 Remove 按钮。 您会看到购物车更新,但不会看到价格。 total
状态值在更新期间被保留。 该值仅用于示例目的; 使用此应用程序,您可能希望更新 state
对象的两个属性。 但是您通常会拥有具有不同职责的有状态属性的组件,您可以通过将它们排除在更新的对象之外来使它们持久存在。
这一步的变化是静态的。 您可以提前确切地知道这些值是什么,并且不需要从 state
重新计算它们。 但是,如果产品页面有很多产品并且您希望能够多次添加它们,则传递静态对象并不能保证引用最新的 state
,即使您的对象使用this.state
值。 在这种情况下,您可以改用函数。
在下一步中,您将使用引用当前状态的函数更新 state
。
第 4 步 - 使用当前状态设置状态
很多时候,您需要引用以前的状态来更新当前状态,例如更新数组、添加数字或修改对象。 为了尽可能准确,您需要引用最新的 state
对象。 与使用预定义值更新 state
不同,在此步骤中,您将向 setState
方法传递一个函数,该方法将当前状态作为参数。 使用此方法,您将使用当前状态更新组件的状态。
使用函数设置 state
的另一个好处是提高了可靠性。 为了提高性能,React 可能会批处理 setState
调用,这意味着 this.state.value
可能不完全可靠。 例如,如果您在多个位置快速更新 state
,则值可能已过期。 这可能发生在数据提取、表单验证或并行发生多个操作的任何情况下。 但是使用具有最新 state
的函数作为参数可确保此错误不会进入您的代码。
为了演示这种形式的状态管理,向产品页面添加更多项目。 首先,打开Product.js
文件:
nano src/components/Product/Product.js
接下来,为不同的产品创建一个对象数组。 该数组将包含产品表情符号、名称和价格。 然后遍历数组以显示带有 Add 和 Remove 按钮的每个产品:
state-class-tutorial/src/components/Product/Product.js
import React, { Component } from 'react'; import './Product.css'; const products = [ { emoji: '🍦', name: 'ice cream', price: 5 }, { emoji: '🍩', name: 'donuts', price: 2.5, }, { emoji: '🍉', name: 'watermelon', price: 4 } ]; export default class Product extends Component { ... render() { return( <div className="wrapper"> <div> Shopping Cart: {this.state.cart.length} total items. </div> <div>Total {this.getTotal()}</div> <div> {products.map(product => ( <div key={product.name}> <div className="product"> <span role="img" aria-label={product.name}>{product.emoji}</span> </div> <button onClick={this.add}>Add</button> <button onClick={this.remove}>Remove</button> </div> ))} </div> </div> ) } }
在此代码中,您使用 map() 数组方法 循环 products
数组并返回将在浏览器中显示每个元素的 JSX。
保存文件。 当浏览器重新加载时,您将看到更新的产品列表:
现在你需要更新你的方法。 首先,更改 add()
方法以将 product
作为参数。 然后,不是将对象传递给 setState()
,而是传递一个函数,该函数将 state
作为参数并返回一个对象,其中 cart
使用新产品和 [ X186X] 更新了新价格:
state-class-tutorial/src/components/Product/Product.js
import React, { Component } from 'react'; import './Product.css'; ... export default class Product extends Component { state = { cart: [], total: 0 } add = (product) => { this.setState(state => ({ cart: [...state.cart, product.name], total: state.total + product.price })) } currencyOptions = { minimumFractionDigits: 2, maximumFractionDigits: 2, } getTotal = () => { return this.state.total.toLocaleString(undefined, this.currencyOptions) } remove = () => { this.setState({ cart: [] }) } render() { return( <div className="wrapper"> <div> Shopping Cart: {this.state.cart.length} total items. </div> <div>Total {this.getTotal()}</div> <div> {products.map(product => ( <div key={product.name}> <div className="product"> <span role="img" aria-label={product.name}>{product.emoji}</span> </div> <button onClick={() => this.add(product)}>Add</button> <button onClick={this.remove}>Remove</button> </div> ))} </div> </div> ) } }
在传递给 setState()
的匿名函数中,确保引用参数 - state
- 而不是组件的状态 - this.state
。 否则,您仍然冒着获取过期 state
对象的风险。 函数中的 state
在其他方面是相同的。
注意不要直接改变状态。 相反,当向 cart
添加新值时,您可以通过对当前值使用 扩展语法 将新的 product
添加到 state
并将新值添加到末尾。
最后,通过更改 onClick()
属性来更新对 this.add
的调用,以采用一个匿名函数,该函数使用相关产品调用 this.add()
。
保存文件。 当您这样做时,浏览器将重新加载,您将能够添加多个产品。
接下来,更新 remove()
方法。 遵循相同的步骤:将 setState
转换为函数,在不改变的情况下更新值,并更新 onChange()
属性:
state-class-tutorial/src/components/Product/Product.js
import React, { Component } from 'react'; import './Product.css'; ... export default class Product extends Component { ... remove = (product) => { this.setState(state => { const cart = [...state.cart]; cart.splice(cart.indexOf(product.name)) return ({ cart, total: state.total - product.price }) }) } render() { return( <div className="wrapper"> <div> Shopping Cart: {this.state.cart.length} total items. </div> <div>Total {this.getTotal()}</div> <div> {products.map(product => ( <div key={product.name}> <div className="product"> <span role="img" aria-label={product.name}>{product.emoji}</span> </div> <button onClick={() => this.add(product)}>Add</button> <button onClick={() => this.remove(product)}>Remove</button> </div> ))} </div> </div> ) } }
为避免改变状态对象,您必须首先使用 spread
运算符对其进行复制。 然后你可以 splice 从副本中取出你想要的项目,并在新对象中返回副本。 通过复制 state
作为第一步,您可以确保不会改变 state
对象。
保存文件。 当您这样做时,浏览器将刷新,您将能够添加和删除项目:
这个应用程序还有一个bug:在remove
方法中,即使项目不在cart
中,用户也可以从total
中减去。 如果您在冰淇淋上单击删除而不将其添加到您的购物车,您的总数将是-5.00。
您可以通过在减去之前检查项目的存在来修复错误,但更简单的方法是通过仅保留对产品的引用而不分离对产品的引用和总成本来保持您的状态对象较小。 尽量避免重复引用相同的数据。 相反,将原始数据存储在 state
(在本例中为整个 product
对象)中,然后在 state
之外执行计算。
重构组件,使 add()
方法添加整个对象,remove()
方法删除整个对象,getTotal
方法使用 cart
:
state-class-tutorial/src/components/Product/Product.js
import React, { Component } from 'react'; import './Product.css'; ... export default class Product extends Component { state = { cart: [], } add = (product) => { this.setState(state => ({ cart: [...state.cart, product], })) } currencyOptions = { minimumFractionDigits: 2, maximumFractionDigits: 2, } getTotal = () => { const total = this.state.cart.reduce((totalCost, item) => totalCost + item.price, 0); return total.toLocaleString(undefined, this.currencyOptions) } remove = (product) => { this.setState(state => { const cart = [...state.cart]; const productIndex = cart.findIndex(p => p.name === product.name); if(productIndex < 0) { return; } cart.splice(productIndex, 1) return ({ cart }) }) } render() { ... } }
add()
方法与之前的方法类似,只是删除了对 total
属性的引用。 在 remove()
方法中,您可以使用 findByIndex
找到 product
的索引。 如果索引不存在,您将获得 -1
。 在这种情况下,您使用 条件语句 不返回任何内容。 通过不返回任何内容,React 将知道 state
没有改变并且不会触发重新渲染。 如果返回 state
或空对象,它仍然会触发重新渲染。
使用 splice()
方法时,您现在将 1
作为第二个参数传递,这将删除一个值并保留其余值。
最后,使用 reduce() 数组方法计算 total
。
保存文件。 当您这样做时,浏览器将刷新,您将获得最终的 cart
:
您传递的 setState
函数可以具有当前道具的附加参数,如果您有需要引用当前道具的状态,这将很有帮助。 您也可以将回调函数作为第二个参数传递给 setState
,无论您是否为第一个参数传递对象或函数。 当您在从 API 获取数据后设置 state
并且需要在 state
更新完成后执行新操作时,这特别有用。
在此步骤中,您学习了如何根据当前状态更新新状态。 您将一个函数传递给 setState
函数并在不改变当前状态的情况下计算新值。 您还学习了如何在没有更新的情况下退出 setState
函数以防止重新渲染,从而稍微增强性能。
结论
在本教程中,您开发了一个基于类的组件,该组件具有您已静态更新并使用当前状态的动态状态。 您现在拥有制作复杂项目以响应用户和动态信息的工具。
React 确实有一种使用 Hooks 管理状态的方法,但是如果您需要使用必须基于类的组件,例如使用 componentDidCatch
方法的组件,了解如何在组件上使用状态会很有帮助.
管理状态是几乎所有组件的关键,也是创建交互式应用程序所必需的。 有了这些知识,您可以重新创建许多常见的 Web 组件,例如滑块、手风琴、表单等。 然后,您将使用与使用挂钩构建应用程序或开发从 API 动态提取数据的组件相同的概念。
如果您想查看更多 React 教程,请查看我们的 React 主题页面,或返回 如何在 React.js 系列页面中编码 。