目录:
1. 如何定义 State
2. 如何修改 State
3. State 的不可变原则
React 的核心思想是组件化的思想,应用由组件搭建而成。组件中最重要的概念是 State,State 是组件的 UI 数据模型,是组件渲染时的一个数据依据。
State 代表组件 UI 状态的变化。
在 React 应用中,当涉及到组件 UI 变化的时候,必须要将变量定义成 State 值。
定义 State 值的方式可以是在 constructor 中初始化 state值。
例 子
父组件是 App.js,子组件是 listItem.jsx。目前在页面上点击“+”“-”按钮可以改变 count 的值,但是 UI 不会变化。
import React, { Component } from 'react'; import ListItem from './components/listItem' const listData = [ { id: 1, name: '红苹果', price: 2 }, { id: 2, name: '青苹果', price: 3 }, ] class App extends Component { renderList(){ return listData.map( item => { return( <ListItem key={item.id} data={ item } onDelete={this.handleDelete} /> ) }) } handleDelete = (id) => { console.log( 'id:', id ); } render() { return( <div className="container"> { listData.length === 0 && <div className="text-center">购物车是空的</div> } { this.renderList() } </div> ) } } export default App;
import React, { Component } from 'react'; import style from './listItem.module.css'; import classnames from 'classnames/bind' const cls = classnames.bind(style); let count = 0; class ListItem extends Component { doSomethingWithCount(){ if(count<0){ count = 0 } } handleDecrease = () => { count --; this.doSomethingWithCount(); console.log( count ); } handleIncrease = () => { count ++; console.log( count ); } render() { return ( <div className="row mb-3"> <div className="col-6 themed-grid-col"> <span className={ cls('title', 'list-title') }> {this.props.data.name} </span> </div> <div className="col-1 themed-grid-col">¥{this.props.data.price}</div> <div className={`col-2 themed-grid-col${count ? '' : '-s'}`}> <button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button> <span className={ cls('digital') }>{count}</span> <button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button> </div> <div className="col-1 themed-grid-col"> <button onClick={()=>{this.props.onDelete(this.props.data.id)}} className="btn btn-danger btn-sm" type="button" >删除 </button> </div> </div> ); } } export default ListItem;
页面表现:
为了让 count 在 UI 上发生变化,在 listItem.jsx 中定义 State。在 constructor 里使用 this.state,赋给 this.state 一个对象,并将变量 count 写到对象里。
引用 state 的时候,不能直接使用 count ,而是应该使用 this.state.count 去引用。
例子
下面代码演示如何将变量 count 定义为 state。
( 因为在几个函数里对 count 的修改方式错误,所以代码会报错,在笔记下面有修改 state 的部分。)
import React, { Component } from 'react'; import style from './listItem.module.css'; import classnames from 'classnames/bind' const cls = classnames.bind(style); class ListItem extends Component { constructor(props){ super(props) this.state = { count : 0 } } doSomethingWithCount(){ if(count<0){ count = 0 } } handleDecrease = () => { count --; this.doSomethingWithCount(); console.log( count ); } handleIncrease = () => { count ++; console.log( count ); } render() { return ( <div className="row mb-3"> <div className="col-6 themed-grid-col"> <span className={ cls('title', 'list-title') }> {this.props.data.name} </span> </div> <div className="col-1 themed-grid-col">¥{this.props.data.price}</div> <div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}> <button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button> <span className={ cls('digital') }>{this.state.count}</span> <button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button> </div> <div className="col-1 themed-grid-col"> <button onClick={()=>{this.props.onDelete(this.props.data.id)}} className="btn btn-danger btn-sm" type="button" >删除 </button> </div> </div> ); } }
怎么样定义需要一个 State
定义 State 值之前需要做一些思考。
第一、判断这个状态是否需要通过 props 从父组件获取,从父组件中获取并且在整个组件中不发生变化,如果是这样的话,可能就不需要去定义 state。第二、判断是否可以通过其它 state 和 props 计算得到,如果可以,可能就不需要去定义 state。第三、判断是否在 render 方法中使用,如果不在 render 方法中使用,可能就不需要去定义 state。
比如上面的例子,将 count 定义成 state,是因为 count 不能从父组件中获取,用户的交互使 count 发生变化,并且在 render 方法中会使用到 count。
下面说明如何修改 state 的值,从而改变 UI 的状态。
例 子
在 “-”、“+” 的 button 元素上添加了点击事件,在事件回调函数中对变量 count 进行操作。如果 count 是 state 值,只能通过 setState 方法对 count 进行修改。在本例中,给 setState 方法赋一个对象作为参数,在对象中修改 count 的值。
import React, { Component } from 'react'; import style from './listItem.module.css'; import classnames from 'classnames/bind' const cls = classnames.bind(style); class ListItem extends Component { constructor(props){ super(props) this.state = { count : 0 } } handleDecrease = () => { this.setState({ count : this.state.count - 1 }) } handleIncrease = () => { this.setState({ count : this.state.count + 1 }) } render() { return ( <div className="row mb-3"> <div className="col-6 themed-grid-col"> <span className={ cls('title', 'list-title') }> {this.props.data.name} </span> </div> <div className="col-1 themed-grid-col">¥{this.props.data.price}</div> <div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}> <button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button> <span className={ cls('digital') }>{this.state.count}</span> <button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button> </div> <div className="col-1 themed-grid-col"> <button onClick={()=>{this.props.onDelete(this.props.data.id)}} className="btn btn-danger btn-sm" type="button" >删除 </button> </div> </div> ); } } export default ListItem;
页面表现:
初学者可能会犯的错误
错误1. 通过类似“ this.state.count -- “ 这样的方式修改 state 值,这完全没有作用。
错误2. 在 constructor 中使用 setState 方法进行状态改变,这会报错。
这是因为 React 执行 setState 的时候会优化 setState 执行的时机,有可能会因为性能优化将多个 setState 合并在一起去执行,所以 setState 不是同步执行的。所以,不要依赖当前的 state 去计算另外一个 state ,如果必须要拿到修改完成之后的 state 可以通过回调函数的方式去实现。
例 子
点击 “ + ” 按钮时,会调用 handleIncrease 函数,在函数中使用 setState 方法修改 count ,让 count 加 1。为了演示 setState 是异步的,在使用 setState 修改 count 前后添加标记,输出 count。
import React, { Component } from 'react'; import style from './listItem.module.css'; import classnames from 'classnames/bind' const cls = classnames.bind(style); class ListItem extends Component { constructor(props){ super(props) this.state = { count : 0 } } handleDecrease = () => { this.setState({ count : this.state.count - 1 }) } handleIncrease = () => { console.log('step 1', this.state.count); this.setState({ count : this.state.count + 1 }) console.log('step 2', this.state.count); } render() { return ( <div className="row mb-3"> <div className="col-6 themed-grid-col"> <span className={ cls('title', 'list-title') }> {this.props.data.name} </span> </div> <div className="col-1 themed-grid-col">¥{this.props.data.price}</div> <div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}> <button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button> <span className={ cls('digital') }>{this.state.count}</span> <button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button> </div> <div className="col-1 themed-grid-col"> <button onClick={()=>{this.props.onDelete(this.props.data.id)}} className="btn btn-danger btn-sm" type="button" >删除 </button> </div> </div> ); } } export default ListItem;
页面表现:
点击按钮在控制台输出的 count 均为 0 ,说明 setState 是异步的,因为如果不是异步的话 step 2 输出的 count 应该是 1。
如果必须要使用变化后的 state 值进行下一步的操作,可以给 setState 方法传入第二个参数,可以传入一个回调函数,这个回调函数执行的时机就是 setState 已经执行完成的一个时机。
例 子
import React, { Component } from 'react'; import style from './listItem.module.css'; import classnames from 'classnames/bind' const cls = classnames.bind(style); class ListItem extends Component { constructor(props){ super(props) this.state = { count : 0 } } handleDecrease = () => { this.setState({ count : this.state.count - 1 }) } handleIncrease = () => { console.log('step 1', this.state.count); this.setState({ count : this.state.count + 1 }, ()=>{ console.log('step 3', this.state.count); }) console.log('step 2', this.state.count); } render() { return ( <div className="row mb-3"> <div className="col-6 themed-grid-col"> <span className={ cls('title', 'list-title') }> {this.props.data.name} </span> </div> <div className="col-1 themed-grid-col">¥{this.props.data.price}</div> <div className={`col-2 themed-grid-col${this.state.count ? '' : '-s'}`}> <button onClick={ this.handleDecrease } type="button" className="btn btn-primary">-</button> <span className={ cls('digital') }>{this.state.count}</span> <button onClick={ this.handleIncrease } type="button" className="btn btn-primary">+</button> </div> <div className="col-1 themed-grid-col"> <button onClick={()=>{this.props.onDelete(this.props.data.id)}} className="btn btn-danger btn-sm" type="button" >删除 </button> </div> </div> ); } } export default ListItem;
页面表现:
点击按钮之后,可以看到 step 3 输出的 count 值加了 1。
3. state 的更新是一个浅合并(shallow merge)的过程
例 子
如果 state 有多个属性,比如说有 count、price。在只需要修改 count 而不需要修改 price 时,只需要给 setState 传入 count 即可,不需要传入整个 state。
在创建新状态的时候要遵循一些原则。
当状态发生变化的时候,根据状态的类型可以分为以下三种情况:
1. 值类型 : 数字、字符串、布尔值、null、undefined
2. 数组类型
3. 对象
创建新的状态
状态是不可变类型,直接给要修改的状态赋予新值即可。使用 setState 的时候可以直接修改 state 值,比如说数字、字符串、布尔值等。
有 2 种方法:① concat;② 使用“...”解构符去生成新数组
① concat
concat 方法用于合并两个或多个数组,此方法不会更改原来的数组,而是会返回一个新数组。
注意,不要使用诸如 push 、pop、shift、unshift、spice 等方法,因为这些方法都是在原数组的基础上修改的。可以使用 concat、slice、filter 这些方法,这些方法会返回一个新数组。
② 使用 “...” 解构符去生成新数组
使用 “...” 解构符去生成新数组原理跟使用 concat 是一样的。生成 _books 新数组之后,使用 setState 将 _books 赋予给原来的状态。
通常会使用 Object.assign 方法生成新对象,Object.assign 用于将所有可枚举属性的值从一个或多个对象复制到目标对象。
如果要进行深拷贝,还是要使用 JSON.stringify() 等方法。
① Object.assign
下面例子使用 Object.assign 合并对象 {}、this.state.item、{price:9000},将 this.state.item 跟 {price:9000} 合并到空对象 { } 中,从而去生成一个新对象。
② “...” 解构符
使用 “...” 解构符道理跟使用 Object.assign 是一样的。
小结
创建新的状态对象的关键是避免使用会修改原对象的方法,而是使用可以返回一个新对象的方法。推荐使用类似 Immutable 这样的 JS 库,可以方便地创建和管理不可变的数据。
1. 可变的
state 用于组件保存、控制、修改
2. 组件内部
state 在组件内部初始化,可以被组件自身修改,但是外部不能访问也不能修改,所以,可以认为 state 是一个局部只能被组件自身控制的数据源
3. 交互或其他 UI 造成的数据更新
state 是交互或其他 UI 造成的数据更新。通常情况下,state 的这种变化会触发组件的重新渲染。
1. 在组件内部不可变
props 是父组件传入的参数对象,它是外部传来的配置参数。
2. 父组传入
在组件内部无法控制也无法修改,除非外部组件主动传入新的 props,否则组件的 props 保持不变。
3. 简单的数据流
可以把 props 看成是一个从上而下的一个简单的数据流。
组件中 state 的数据可以通过 props 传入子组件,反过来,组件也可以用外部传入的 props 来初始化自己的 state。
state 是让组件控制自己的状态,而 props 是让外部对组件进行配置。