React中的state
在上一节中我们学习了如何使用React的props
来改变组件(UI)以及如何在React中使用props
。除了props
可以改变组件之外还可以通过React的state
,而且state
也是React的重要概念和必须掌握的知识之一。通过上一节的学习,我们了解到props
是纯静态的改变UI,而今天要聊的state
是动态的,让你的组件真的能动起来,可以让用户和你的产品进行交互。用户和你的应用程序的每次交互都有可能更改state
,从而导致对应的UI更改(或内容的重新渲染)。
如果你从未接触过或者想学习这方面的知识的话,欢迎继续往下阅读。
state
的解释
通过对props
的学习我们知道可以通过props
给组件透传值,从而更改UI效果。比如上节中提到的房子案例:
整个房子是一个<House />
组件,它由房顶(<Roof />
)、墙(<Wall />
)、门(<Door />
)和窗户(<Window />
)。每个组件都有自己的props
,比如,Roof
有roofSrc
(房顶图片)和color
(改变房顶的颜色)。在使用的时候,可以通过color
这个props
来改变房顶的颜色(正如上图你所看到的,有blue
、red
和salmon
等)。
就房子这个示例而言,Roof
、Wall
是静态的(你总不可能随意去拆了房顶和墙重建吧),但Door
和Window
是可以具有交互的(人可以打开门或窗户,这是很正常的形为)。如果把这个House
拿到Web应用程序来说,Roof
和Wall
是不具交互性的,仅仅是展示而以;但Door
和Window
是具有交互性的,可以根据用户的操作行为做出相应的变更。
在上面的示例,我们看到House
有打开门的,也有关闭门的两个效果。只不过上面的示例是通过给Door
组件传递了两个不同的图片(props
是同一个)。具体代码如下:
这是纯静态展示,但我们应该给Web应用程序(房子House
)赋予更强大的灵魂,让应用程序能和自己的用户交互起来。也就是说,房子的门应该是由用户的操作来完成的。比如说,用户点了一下门(有可能触发了一个click
事件),门就打开了,用户再点一下门,门又关闭了。那么在React中要实现这样的一个效果,我们不能仅依赖于props
来完成了。我们需要借助React的state
来完成这样的功能。
换句话说,如果React的props
持有不可变的数据来改变组件渲染的话,那么state
将存储关于组件的数据,这些数据可能会随时间变化。数据的变化可以以用户操作(事件),比如用户鼠标点击了一下左键,用户键盘按下了(具体的可以看看React中的事件一文)等。在React中处理组件的状态通常会涉及组件的默认状态、访问当前状态和更新状态。
比如上面讲到的房子这个组件,我们就可以通过state
来让用户具有可操作性(开门和关门之间切换)。
React的state
如何使用
还是拿<House />
来举例,看看在React中怎么使用state
。在使用state
之前首要做的第一件事情就是初始化state
数据,然后才能在render()
中使用它。为了设置初始状态,在构造函数constructor
中使用this.state
来初始化state
。如果你从父组件获取逻辑的话,一定要调用super()
方法:
class House extends React.Component {
constructor(props) {
super(props)
this.state = {
// ...
}
}
render() {
// ...
}
}
为什么要使用
super()
,有关于这方面更详细的介绍,可以阅读《为什么我们要写 super(props) ?》一文。
上面是使用class
创建组件时声明state
的方法,如果我们使用函数组件或者现在流行的React Hooks的话,使用方式会略有不同,这里暂时不表,后续会和大家一起学习和探讨怎么在React Hooks中使用props
和state
。
在设置初始状态时我们可以添加逻辑,比如上面的示例中,设置了isOpen
的初始值:
class House extends React.Component {
constructor(props) {
super(props)
this.state = {
isOpen: false
}
}
render() {
// ...
}
}
通常在使用类创建组件时会调用constructor()
构造函数。大多数情况下,当调用constructor()
时,super()
方法是在构造函数内部调用的,否则父类的构造函数将不会执行。另外this.state
必须是一个对象。
如果要更新状态的话,需要使用this.setState(data, callback)
方法。调用此方法时,React将data
与当前状态合并并调用render()
。然后,React调用callback
。
在setState()
中使用回调非常重要,因为这些方法是异步执行的。你可以使用回调来确保在使用新状态之前它是可用的。如果你依赖一个新状态,而不等setState()
完成其工作,那么你将同步地处理异步操作,这意味着当前状态仍然是旧状态时,可能会出现错误。
此值的变化取决于从何处调用函数。与我们的组件相关的上下文被绑定到this
,以确保它与我们的组件是同步的。
class House extends React.Component {
constructor (props) {
super(props)
this.state = {
isOpen: false
}
this.handleClick = this.handleClick.bind(this)
}
handleClick = () =>{
this.setState({
isOpen: !this.state.isOpen
})
}
render() {
// ...
}
}
正如上面这个示例所示,state
中的isOpen
可以用于反映组件的UI变化。加上前面有关于props
的学习,那么组件中用到的一个变量是应该作为组件的state
还是props
呢?简单地说,在实际开发者,我们怎么来选择何种情况下应该将一个变量定义为组件的state
,而不是props
。
我们在使用组件开发的时候,可以根据下面几个规则来进行判断:
- 该变量是否通过
props
从父组件中获取,如果是,那么它不是一个state
- 该变量是否在组件的整个生命周期中都保持不变,如果是,那么它不是一个
state
- 该变量是否可能通过其他状态(
state
)或者属性(props
)计算得到,如果是,那么它不是一个state
- 该变量是否在组件
render()
方法中使用,如果不是,那么它不是一个state
简单地说,并不是组件中用到的所有可变的(即变量)都是组件状态!。
通过上面的介绍,我们对React的state
有了一个初步的认识,接下来我们来学习如何正确的使用React的state
。
如何正确的使用React的state
在React中使用state
有些细节是需要注意的,主要体现在以下几个方面。
不要直接修改state
在React中直接修改state
的话,组件并不会重新重发render()
。例如:
this.state.isOpen = true
这是一种错误的使用方式。正确的修改方式是使用setState()
:
this.setState({
isOpen: true
})
构造函数是唯一可以给
this.state
赋值的地方。
state
的更新可能是异步的
在React中调用setState
时,组件的state
并不会立即改变,setState
只是把要修改的状态放到一个队列中,React会优化真正的执行时机,并且React会出现性能方面的考虑,可能会将多次setState
的状态修改合并成一次状态修改,然后再触发组件更新。这一点需要好好注意,比如像下面这样的一段代码:
// ...
handleClick = () =>{
console.log(this.state.isOpen)
this.setState({
isOpen: !this.state.isOpen
})
console.log(this.state.isOpen)
}
// ...
你会发现打印出来的值都是falsee
,即使我们中间已经setState
过一次了。出现这种现象只是因为React的setState
把你传进来的状态缓存起来了,稍后才会帮你更新到state
上,所以你获取到的还是原来的isOpen
。所以如想在setState
之后使用新的state
来做后续运算是做不到的。
也就是说,在React中不要依赖前当的state
来计算下一个state
。当执行真正状态修改时,依赖的this.state
并不能保证是最新的state
,因为React会把多次state
的修改合并成一次,这时this.state
将还是这几次state
修改前的state
。另外需要注意的是,同样不能依赖于当前props
计算下个状态,因为props
一般也是从父组件的state
中获取,依赖无法确定在组件状态更新的值。
这也就自然的引出了setState
的第二种使用方式,可以接受一个函数作为参数。这个函数有两个参数,第一个是当前最新状态(本次组件状态修改后的状态)的前一个状态preState
(本次组件状态修改前的状态),第二个参数是当前最新的属性props
。如下所示:
// ...
handleClickOnLikeButton () {
this.setState((prevState) => {
return { count: 0 }
})
this.setState((prevState) => {
return { count: prevState.count + 1 } // 上一个 setState 的返回是 count 为 0,当前返回 1
})
this.setState((prevState) => {
return { count: prevState.count + 2 } // 上一个 setState 的返回是 count 为 1,当前返回 3
})
// 最后的结果是 this.state.count 为 3
}
// ...
state
的更新是一个浅合并的过程
当调用setState
修改组件状态时,只需要传入发生改变的state
,而不是组件完整的state
,因为组件state
更新是一个浅合并的过程。
例如,你的 state
包含几个独立的变量:
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
然后你可以分别调用 setState()
来单独地更新它们:
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
这里的合并是浅合并,所以 this.setState({comments})
完整保留了 this.state.posts
, 但是完全替换了 this.state.comments
。
React的state
最好是不可变的
React官方建议把state
当作不可变(Immutable
)对象,一方面是如果直接修改this.state
,组件并不会重新渲染;另一方面state
中包含的所有状态都应该是不可变对象。
这样一来,当state
中的某个状态发生变化,我们应该重新创建这个状态对象,而不是直接修改原来的状态,正如上面的代码所示:
// 错误的
this.state.isOpen = false
// 正确的
this.setState({
isOpen: false
})
那么,当状态发生变化时,如何创建新的状态呢?根据状态的类型,可以分为三种情况。
状态的类型是不可变类型
当状态类型是不可变类型(即Number
、String
、Boolean
、null
或undefined
)时,可以直接给要修改的状态赋一个新值。比如上面House
组件这个示例,其中isOpen
这个状态就是一个布尔值。如果要修改的话,可以像下面这样来修改:
this.setStatee({
isOpen: !this.state.isOpen
})
状态的类型是数组
如果状态的类型是一个数组时,如果给这个数组增加一个新的项目,可以使用数组的concat
方法或ES6的数组组的扩展语法(...
)。比如我们有一个lists
这个状态,如果希望给这个lists
增加新的一项,可以像下面这样来操作:
// 将state先赋值给另外的变量,然后使用concat创建新数组
var lists = this.state.lists
this.setState({
lists: lists.concat(['React'])
})
// 使用preState,concat创建新数组
this.setState( preState => ({
lists: preState.lists.concat(['React'])
}))
// ES6的扩展语法
this.setState(preState => ({
lists: [...preState.lists, 'React']
}))
如果我们想从lists
中截取部分元素作为新状态时,可以使用数组的slice
方法:
// 将state先赋值给另外的变量,然后使用slice创建新数组
var lists = this.state.lists
this.setState({
lists: lists.slice(1, 3)
})
// 使用preState、slice创建新数组
this.setState( preState => ({
lists: preState.lists.slice(1,3)
}))
如果希望从lists
中过滤部分元素后,作为新状态时,可以使用数组的filter
方法:
// 先将state赋值给另外的变量,然后使用filter创建新数组
var lists = this.state.lists
this.setState({
lists: lists.filter(item => {
return item != 'React'
})
})
// 使用preState、filter创建新数组
this.setState(preState => ({
lists: preState.lists.filter(item => {
return item != 'React'
})
}))
这里将会涉及到一些有关于JavaScript数组的操作方法,但要注意的是不要使用像push
、pop
、shift
、unshift
和splice
等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concat
、slice
和filter
则会返回一新数组。
如果我想彻底的搞清楚数组的哪些方法只是修改原数组,哪些方法则是会创建一个新数组,建议花点时间阅读下面相关教程:
- All about Immutable Arrays and Objects in JavaScript
- Immutability in React: There’s nothing wrong with mutating objects
- ES6 Way to Clone an Array
- WHY USE IMMUTABLE VS MUTABLE CODE IN JAVASCRIPT?
- Four Ways to Immutability in JavaScript
- Immutable changes to Objects and Arrays in JavaScript
- JavaScript数组的那些事儿
状态的类型是普通的对象
在React中的state
还可能是普通的对象(不包含字符串和数组等)。我们可以使用ES6的Object.assgin
和对象扩展语法等来改变对象:
// 将state先赋值给另外的变量,然后使用Object.assign创建新对象
var obj = this.setState.obj
this.setState({
obj: Object.assign({}, obj, {name: 'React'})
})
// 使用preState和Object.assign创建新对象
this.setState(preState => ({
obj: Object.assign({}, obj, {name: 'React'})
}))
// 将state先赋值给另外的变量,然后使用对象扩展语法创建新对象
var obj = this.setState.obj
this.setState({
obj: {...obj, {name: 'React'}}
})
// 使用preState和对象扩展语法创建新对象
this.setState(preState => ({
obj: {...preState.obj, {name: 'React'}}
}))
和创建数组一样,创建新的状态对象关键也是避免使用直接修改原对象的方法,而是使用可以返回一个新对象的方法。这样做一方面是因为不可变对象方便管理和调试,另一方面出于性能考虑,当对象组件状态都是不可变对象时,我们在组件的shouldComponentUpdate
方法中,仅需要比较状态的引用就可以判断状态是否真的改变,从而避免不必要的render
调用。当我们使用React 提供的PureComponent
时,更是要保证组件状态是不可变对象,否则在组件的shouldComponentUpdate
方法中,状态比较就可能出现错误,因为PureComponent
执行的是浅比较(比较对象的引用)。
数据是向下流动的
不管是父组件还是子组件都无法知道某个组件是有状态还是无状态的,并且它们也并不关心它是函数组件还是 class
组件。
这就是为什么称state
为局部的或是封装的原因。除了拥有并设置了它的组件,其他组件都无法访问。
组件可以选择把它的state
作为props
向下传递到它的子组件中。在前面创建的<House />
组件中的<Door />
组件就有这样的使用:
<Door doorOpen={this.props.doorOpen} doorClose={this.props.doorClose} isOpen={this.state.isOpen} onClick={this.handleClick} />
将状态<House />
组件中的状态this.state.isOpen
当作<Door />
组件的props
(isOpen
)传入。这通常会被叫做“自上而下”或是“单向”的数据流。任何state
总是所属于特定的组件,而且从该state
派生的任何数据或UI只能影响树中“低于”它们的组件。
如果你把一个以组件构成的树想象成一个props
的数据瀑布的话,那么每个组件的state
就像是在任意一点上给瀑布增加额外的水源,但是它只能向下流动。
如何从子组件中更改父组件的state
在学习props
的时候,我们知道,在React中无法将props
从子组件传递给父组件,但是可以从父组件向子组件传递一个函数,而子组件使用这些函数,并且这些函数可能会更改上面父组件中的state
。一旦状态改变了,状态就会再次作为props
传递下去给子组件。所有受影响的组件就会重新渲染。那么在React中,如何从子组件中来改更父组件的state
呢?
我们用一个计数器Counter
组件为例向大家演示。这个计数器很简单,主要包含以下几个部分:
为了更好的演示如何从子组件改变父组件,该示例中把按钮创建成一个<Button />
组件,同时给<Button />
组件透传两个props
,分别是用于点击事件的onClick
和按钮文本buttonText
:
const Button = ({onClick, buttonText}) => {
return <button onClick={onClick}><span>{buttonText}</span></button>
}
该组件很简单而且该组件没有自己的任何状态,也不会使用任何生命周期方法,因此它将作为无状态功能组件工作。
接下来,我们创建<Counter />
组件,并且将前面创建好的<Button />
放到该组件中。因为我们给<Button />
组件透传了两个props
,所以我们可以重复使用<Button />
组件。对于<Counter />
组件而言,他要做的事情也很简单,用户点击“+
”(会执行increment()
方法)时,数字count
会递增,点击“-
”(会执行decrement()
方法)时,数字count
会递减。也就是说,在<Counter />
组件中我们需要两个方法increment()
和decrement()
和一个状态count
。代码如下:
class Counter extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0
}
this.increment = this.increment.bind(this)
this.decrement = this.decrement.bind(this)
}
increment () {
this.setState({
count: this.state.count + 1
})
}
decrement () {
this.setState({
count: this.state.count - 1
})
}
render() {
return (
<>
<h3>Counter Component with React</h3>
<div className="control">
<Button onClick={this.increment} buttonText="+" />
<span>{this.state.count}</span>
<Button onClick={this.decrement} buttonText="-" />
</div>
</>
)
}
}
效果如下:
由于increment()
和decrement()
函数控制父组件中的状态,并且将该函数绑定到<Button />
组件的click
事件上,通过onClick
这个props
将方法传给按钮组件,并让按钮组件调用对应绑定的函数。从而达到了从子组件中改变父组件状态的功能。
setState()
vs useState()
在前面的示例中我们看到setState()
出现过很多次,而在React中它又被称之为函数式(Functional)setState()
。@Dan Abramov是这样描述的:
在**函数式
setState()
**模式中,组件state
变化的声明可以和组件类本身独立开来。
听上去很复杂,事实上要搞清楚setState()
对于初学者来说也是不简单的一件事情。@Dan Abramov就有一篇文章《setState
如何知道该做什么?》深入的阐述了setState()
。当然,有关于这方面的介绍也很多,要是你感兴趣的话,也可以阅读下面这些文章:
- How to Use the
setState
Callback in React - How to become a pro with React
setState()
in 10 minutes - Understanding React
setState
- React
setState
usage and gotchas - React
setState()
withprevState
and Object Spread Operator - An imperative guide to
setState
in React - React 未来之函数式
setState
- React 组件生命周期函数里
setState
调用分析
嗯?setState()
和state
一样,在React中是必须掌握的知识点之一,需要花更多的时间去学习和了解。这里暂不做更多的阐述,如果你感兴趣,持续关注后续的更新。在这里我们只需要记住,在class
创建的组件中,可以使用setState()
管理组件的state
。
这已上升到React的另一个话题,状态管理。
再强调一下,作为初学者,我们只需要简单的知道setState()
的作用机制:传递给它一个对象,该对象含有 state
中你想要更新的部分。换句话说,该对象的键(keys
)和组件 state
中的键相对应,然后 setState()
通过将该对象合并到 state
中来更新(或者说 sets
)state
。
随着React Hooks的到来,你可能更喜欢使用Hooks来创建你的组件。那么在React Hooks中有一个类似于class
类组件中的setState()
,那就是useState()
。
useState()
是Reack Hooks内置的一个钩子函数,可以从react
包中引入。它允许你向功能组件(Functional Components)添加state
。使用函数组件(Function Components)中的useState()
钩子可以创建一段状态,无需切换到类组件。
函数处理状态和类处理状态还是有一些区别:
- 在类组件中,状态是使用
this.state
访问的对象;只需在此对象上添加属性来初始化状态,然后使用setState()
来更改它 - 在功能组件中使用
useState
,该状态不一定是对象,它可以是数组、数字、布尔值或字符串等。可以对useState()
进行多次调用,以创建具有初始值的单个状态块,以及稍后用于更改状态的函数
在React中,useState()
钩子将初始状态作为参数,仅在React组件第一次渲染时使用,并返回一个包含两个值的数组:当前状态和状态更新函数。当前状态用于React组件某个地方显示它,而状态更新函数用于更改当前状态。比如上面的计数器组件Counter
我们就可以像下面这样来改造:
const Button = ({onClick, buttonText}) => {
return <button onClick={onClick}><span>{ buttonText }</span></button>
}
const Counter = () => {
const [count, setCount] = React.useState(0)
const increment = () => {
setCount(count + 1)
}
const decrement = () => {
setCount(count - 1)
}
return (
<>
<h3>Counter Component with React</h3>
<div className="control">
<Button onClick={increment} buttonText="+" />
<span>{count}</span>
<Button onClick={decrement} buttonText="-" />
</div>
</>
)
}
最终效果如下:
你可能想知道useState()
钩子函数更多的知识点吧,如果是的话,请继续关注后续的更新,也可以阅读下面相关文章:
- Understanding React Hooks —
useState
- React Hooks — How To Use
useState
anduseEffect
Example - Simplifying React State and the
useState
Hook - How to
useState
in React? - A guide to
useState
in React - 4 Examples of the
useState
Hook - React
useState
Hook
React组件状态管理
上一节中的setState()
和useState()
都只是React中组件状态管理方式中的一种。其实除了文章中提到的这两个之外,React状态管理还可以有其他的方法,比如useReducer()
、useContext()
、Local State、Global State和Context API或者第三方插件库,比如Redux、Mobx和RxJS等。
@BOBI.INK在他的组件设计系列教程的最后一篇就详细的和大家介绍**组件状态管理**。这篇教程写得非常详细,但对于初学者来说(至少对于我自己)理解起来学是很吃力的。另外向大家推荐一些其他相关的教程:
- React State: Choose Wisely
- React State
- Don't Sync State. Derive It!
- Global state management with React Hooks
- Application State Management with React
另外向大家推荐一张@Stephan Meijer在推特上分享的一张有关于React状态管理的图:
小结
经过上一篇props
和这篇的学习,我们对React中的props
和state
有了一个初步的概念。知道怎么通过props
和state
来更改React组件UI的渲染。事实上,两者的结合能让我们的组件变得更可定制,更灵活,复用性更强。当然,这两篇学习的知识都是props
和state
的皮毛,如果我们要彻底的掌握props
和state
,还需要继续深入的学习。那么在后续的更新中,将会和大家继续探讨props
和state
相关的知识。如果你对这方面的知识感兴趣的话,欢迎持续关注后续的更新。如果你对这方面有较好的建议和经验,欢迎在下面的评论中与我们一起分享。