作者选择Creative Commons接受捐赠,作为Write for DOnations计划的一部分。
介绍
在React 中,状态是指一种结构,用于跟踪应用程序中数据随时间的变化情况。管理状态是 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 和创建本地开发环境或如何在 Ubuntu 18.04 上安装 Node.js 的使用 PPA 安装部分中的步骤进行操作。
-
在本教程中,您将使用Create React App创建应用程序。您可以在如何使用 Create React App设置 React 项目中找到使用 Create React App 安装应用程序的说明。
-
您还需要具备 JavaScript 的基本知识(可以在How To Code in JavaScript 中找到),以及 HTML 和 CSS 的基本知识。关于 HTML 和 CSS 的一个很好的资源是Mozilla Developer Network。
第 1 步 – 创建一个空项目
在这一步中,您将使用Create React App创建一个新项目。然后,您将删除引导项目时安装的示例项目和相关文件。最后,您将创建一个简单的文件结构来组织您的组件。这将为您构建本教程的示例应用程序以管理基于类的组件上的状态提供坚实的基础。
首先,创建一个新项目。在您的终端中,运行以下脚本以使用以下命令安装新项目create-react-app
:
- npx create-react-app state-class-tutorial
项目完成后,切换到目录:
- cd state-class-tutorial
在新的终端选项卡或窗口中,使用Create React App start script启动项目。浏览器将自动刷新更改,因此在您工作时保持此脚本运行:
- npm start
您将获得一个正在运行的本地服务器。如果项目没有在浏览器窗口中打开,您可以使用http://localhost:3000/
. 如果您从远程服务器运行它,则地址将为.http://your_domain:3000
您的浏览器将加载一个简单的 React 应用程序,该应用程序包含在 Create React App 中:
您将构建一组全新的自定义组件,因此您需要先清除一些样板代码,以便您可以拥有一个空项目。
首先,src/App.js
在文本编辑器中打开。这是注入页面的根组件。所有组件将从这里开始。您可以App.js
在如何使用 Create React App 设置 React 项目中找到更多信息。
src/App.js
使用以下命令打开:
- nano 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
语句中的所有内容以返回一组空标签:<></>
。这将为您提供一个不返回任何内容的有效页面。最终代码将如下所示:
import React from 'react';
import './App.css';
function App() {
return <></>;
}
export default App;
保存并退出文本编辑器。
最后,删除标志。您不会在您的应用程序中使用它,您应该在工作时删除未使用的文件。从长远来看,这将使您免于困惑。
在终端窗口中输入以下命令:
- rm src/logo.svg
如果您查看浏览器,您将看到一个空白屏幕。
现在您已经清除了示例 Create React App 项目,创建一个简单的文件结构。这将帮助您保持组件隔离和独立。
在目录components
中创建一个名为的src
目录。这将保存您所有的自定义组件。
- mkdir src/components
每个组件都有自己的目录来存储组件文件以及样式、图像和测试。
创建一个目录App
:
- mkdir src/components/App
将所有App
文件移动到该目录中。使用通配符 ,*
选择以不App.
考虑文件扩展名开头的任何文件。然后使用mv
命令将它们放入新目录:
- mv src/App.* src/components/App
接下来,更新 中的相对导入路径index.js
,它是引导整个过程的根组件:
- nano src/index.js
import 语句需要指向目录App.js
中的App
文件,因此进行以下突出显示的更改:
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 步 – 在组件中使用状态
在此步骤中,您将在其类上设置组件的初始状态并引用该状态以显示值。然后,您将创建一个带有购物车的产品页面,该页面使用状态值显示购物车中的商品总数。在这一步结束时,您将了解保存值的不同方式以及何时应该使用状态而不是 prop 或静态值。
构建组件
首先创建一个目录Product
:
- mkdir src/components/Product
接下来,Product.js
在该目录中打开:
- nano src/components/Product/Product.js
首先创建一个没有状态的组件。该组件将包含两部分:购物车(包含商品数量和总价)和产品(包含用于添加和删除商品的按钮)。目前,按钮将没有任何操作。
将以下代码添加到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>
)
}
}
您还包含了几个div
具有JSX类名称的元素,以便您可以添加一些基本样式。
保存并关闭文件,然后打开Product.css
:
- nano src/components/Product/Product.css
提供一些浅色样式以增加font-size
文本和表情符号:
.product span {
font-size: 100px;
}
.wrapper {
padding: 20px;
font-size: 20px;
}
.wrapper button {
font-size: 20px;
background: none;
}
表情符号需要比文本大得多的字体大小,因为它在本例中充当产品图片。此外,您通过将 设置为background
来删除按钮上的默认渐变背景none
。
保存并关闭文件。
现在,在Product
组件中渲染组件,App
以便您可以在浏览器中查看结果。打开App.js
:
- nano src/components/App/App.js
导入组件并渲染它。您还可以删除 CSS 导入,因为您不会在本教程中使用它:
import React from 'react';
import Product from '../Product/Product';
function App() {
return <Product />
}
export default App;
保存并关闭文件。当您这样做时,浏览器将刷新并且您将看到该Product
组件。
设置类组件的初始状态
您的组件值中有两个值将在您的显示中更改:项目总数和总成本。而不是硬编码他们,在这个步骤中,您将它们移到一个对象叫state
。
在state
一个阵营类是控制页面的呈现一个特殊的属性。当您更改状态时,React 知道该组件已过期并会自动重新渲染。当一个组件重新渲染时,它会修改渲染的输出以在state
. 在此示例中,每当您将产品添加到购物车或将其从购物车中删除时,组件都会重新渲染。您可以向 React 类添加其他属性,但它们不会具有触发重新渲染的相同能力。
打开Product.js
:
- nano src/components/Product/Product.js
添加一个属性调用state
到Product
类。然后向state
对象添加两个值:cart
和total
。该cart
会是一个数组,因为它最终可能持有多个项目。该total
会是一个数字。分配这些后,将值的引用替换为:this.state.property
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,因此您需要将代码括在花括号中。您也显示length
了的cart
阵列来获得数组中的项目数的计数。
保存文件。当您这样做时,浏览器将刷新,您将看到与以前相同的页面。
该state
属性是标准的类属性,这意味着它可以在其他方法中访问,而不仅仅是在该render
方法中。
接下来,不是将价格显示为静态值,而是使用toLocaleString
方法将其转换为字符串,该方法会将数字转换为与浏览器区域中显示数字的方式相匹配的字符串。
创建一个名为的方法getTotal()
,该方法接受state
并使用 的数组将其转换为本地化的字符串currencyOptions
。然后,state
用一个方法调用替换JSX 中的引用:
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
最好传递一个包含新值的对象,因为它更易于阅读。如果需要引用当前状态,则传递一个函数以避免对 out-of-date 的任何引用state
。
首先向按钮添加一个事件。如果您的用户单击“添加”,则程序会将项目添加到cart
并更新total
。如果他们点击Remove,它会将购物车重置为一个空数组,并将 重置total
为0
。例如,该程序将不允许用户多次添加项目。
打开Product.js
:
- nano src/components/Product/Product.js
在组件内部,创建一个名为 的新方法add
,然后将该方法传递给Add按钮的onClick
prop :
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
和更新后的价格5
。请注意,您再次使用箭头函数来创建该add
方法。如前所述,这将确保函数this
在运行更新时具有正确的上下文。如果将函数作为方法添加而不使用箭头函数,则setState
如果不将该函数绑定到当前上下文,则该函数将不存在。
例如,如果您以add
这种方式创建函数:
export default class Product extends Component {
...
add() {
this.setState({
cart: ['ice cream'],
total: 5
})
}
...
}
用户在单击“添加”按钮时会收到错误消息。
使用箭头函数可确保您拥有正确的上下文以避免此错误。
保存文件。当您这样做时,浏览器将重新加载,当您单击“添加”按钮时,购物车将使用当前金额更新。
使用该add
方法,您传递了state
对象的两个属性:cart
和total
。但是,您并不总是需要传递一个完整的对象。您只需要传递一个包含要更新的属性的对象,其他所有内容都将保持不变。
要查看 React 如何处理较小的对象,请创建一个名为remove
. 传递一个只cart
包含一个空数组的新对象,然后将该方法添加到Remove按钮的onClick
属性中:
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>
)
}
}
保存文件。当浏览器刷新时,单击添加和删除按钮。您会看到购物车更新,但不会看到价格。在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
接下来,为不同的产品创建一组对象。该数组将包含产品表情符号、名称和价格。然后循环遍历数组以显示带有添加和删除按钮的每个产品:
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
新产品的total
更新和新价格的更新:
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
使用传播语法上的电流值,并增加新的价值到年底。
最后,this.add
通过更改onClick()
prop 以获取一个匿名函数来更新对 的调用,该函数调用this.add()
相关产品。
保存文件。当您这样做时,浏览器将重新加载,您将能够添加多个产品。
接下来,更新remove()
方法。遵循相同的步骤:转换setState
为接受一个函数,在不改变的情况下更新值,然后更新onChange()
prop:
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
操作符复制它。然后你可以从副本中拼接出你想要的项目,并在新对象中返回副本。通过将复制state
作为第一步,您可以确保不会改变state
对象。
保存文件。当您这样做时,浏览器将刷新,您将能够添加和删除项目:
这个应用程序仍然存在一个错误:在该remove
方法中,total
即使项目不在cart
. 如果您在冰淇淋上单击Remove而不将其添加到您的购物车,您的总数将为-5.00。
您可以通过在减去之前检查项目是否存在来修复错误,但更简单的方法是通过仅保留对产品的引用而不是将产品引用和总成本分开来保持状态对象较小。尽量避免对相同数据的双重引用。相反,将原始数据存储在state
– 在这种情况下是整个product
对象 – 然后在state
.
重构组件,使add()
方法添加整个对象,remove()
方法删除整个对象,getTotal
方法使用cart
:
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()
方法中,您可以找到product
with的索引findByIndex
。如果索引不存在,你会得到一个-1
. 在这种情况下,您使用条件语句不返回任何内容。通过不返回任何内容,React 将知道state
没有改变并且不会触发重新渲染。如果你返回state
一个空对象,它仍然会触发重新渲染。
使用该splice()
方法时,您现在将1
作为第二个参数传递,这将删除一个值并保留其余值。
最后,您total
使用reduce()
数组方法计算。
保存文件。当您这样做时,浏览器将刷新,您将获得最终的cart
:
setState
你传递的函数可以有一个当前 props 的额外参数,如果你有需要引用当前 props 的状态,这会很有帮助。您还可以将回调函数传递给setState
作为第二个参数,无论您是为第一个参数传递对象还是函数。当您state
在从 API 获取数据后进行设置并且需要在state
更新完成后执行新操作时,这尤其有用。
在这一步中,您学习了如何根据当前状态更新新状态。您将一个函数传递给该setState
函数并计算新值而不改变当前状态。您还学习了如何在setState
没有更新的情况下退出函数以防止重新渲染,从而稍微提高性能。
结论
在本教程中,您开发了一个具有动态状态的基于类的组件,该组件已静态更新并使用当前状态。您现在拥有了制作能够响应用户和动态信息的复杂项目的工具。
React 确实有一种使用 Hooks 管理状态的方法,但是如果您需要使用必须基于类的组件(例如使用该componentDidCatch
方法的组件),那么了解如何在组件上使用状态会很有帮助。
管理状态是几乎所有组件的关键,也是创建交互式应用程序所必需的。有了这些知识,您可以重新创建许多常见的 Web 组件,例如滑块、手风琴、表单等。然后,您将在使用钩子构建应用程序或开发从 API 动态提取数据的组件时使用相同的概念。
如果您想查看更多 React 教程,请查看我们的React 主题页面,或返回如何在 React.js 系列中编码页面。