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,比如,RoofroofSrc(房顶图片)和color(改变房顶的颜色)。在使用的时候,可以通过color这个props来改变房顶的颜色(正如上图你所看到的,有blueredsalmon等)。

就房子这个示例而言,RoofWall是静态的(你总不可能随意去拆了房顶和墙重建吧),但DoorWindow是可以具有交互的(人可以打开门或窗户,这是很正常的形为)。如果把这个House拿到Web应用程序来说,RoofWall是不具交互性的,仅仅是展示而以;但DoorWindow是具有交互性的,可以根据用户的操作行为做出相应的变更。

在上面的示例,我们看到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中使用propsstate

在设置初始状态时我们可以添加逻辑,比如上面的示例中,设置了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
})

那么,当状态发生变化时,如何创建新的状态呢?根据状态的类型,可以分为三种情况。

状态的类型是不可变类型

当状态类型是不可变类型(即NumberStringBooleannullundefined)时,可以直接给要修改的状态赋一个新值。比如上面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数组的操作方法,但要注意的是不要使用像pushpopshiftunshiftsplice等方法修改数组类型的状态,因为这些方法都是在原数组的基础上修改,而concatslicefilter则会返回一新数组。

如果我想彻底的搞清楚数组的哪些方法只是修改原数组,哪些方法则是会创建一个新数组,建议花点时间阅读下面相关教程:

状态的类型是普通的对象

在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 />组件的propsisOpen)传入。这通常会被叫做“自上而下”或是“单向”的数据流。任何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()。当然,有关于这方面的介绍也很多,要是你感兴趣的话,也可以阅读下面这些文章:

嗯?setState()state一样,在React中是必须掌握的知识点之一,需要花更多的时间去学习和了解。这里暂不做更多的阐述,如果你感兴趣,持续关注后续的更新。在这里我们只需要记住,在class创建的组件中,可以使用setState()管理组件的state

这已上升到React的另一个话题,状态管理

再强调一下,作为初学者,我们只需要简单的知道setState()的作用机制:传递给它一个对象,该对象含有 state 中你想要更新的部分。换句话说,该对象的键(keys)和组件 state 中的键相对应,然后 setState() 通过将该对象合并到 state 中来更新(或者说 setsstate

随着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()钩子函数更多的知识点吧,如果是的话,请继续关注后续的更新,也可以阅读下面相关文章:

React组件状态管理

上一节中的setState()useState()都只是React中组件状态管理方式中的一种。其实除了文章中提到的这两个之外,React状态管理还可以有其他的方法,比如useReducer()useContext()Local StateGlobal StateContext API或者第三方插件库,比如Redux、Mobx和RxJS等。

@BOBI.INK在他的组件设计系列教程的最后一篇就详细的和大家介绍**组件状态管理**。这篇教程写得非常详细,但对于初学者来说(至少对于我自己)理解起来学是很吃力的。另外向大家推荐一些其他相关的教程:

另外向大家推荐一张@Stephan Meijer在推特上分享的一张有关于React状态管理的图:

小结

经过上一篇props和这篇的学习,我们对React中的propsstate有了一个初步的概念。知道怎么通过propsstate来更改React组件UI的渲染。事实上,两者的结合能让我们的组件变得更可定制,更灵活,复用性更强。当然,这两篇学习的知识都是propsstate的皮毛,如果我们要彻底的掌握propsstate,还需要继续深入的学习。那么在后续的更新中,将会和大家继续探讨propsstate相关的知识。如果你对这方面的知识感兴趣的话,欢迎持续关注后续的更新。如果你对这方面有较好的建议和经验,欢迎在下面的评论中与我们一起分享。