2-3-组件数据

今天我们要一起学习一下 React 组件当中对于数据的处理。

  • props

  • state

  • context

props

  • 传入变量

  • 传入函数

  • 传入组件

  • props.children

props其实就是属性Properties的缩写。

在形式上,props之于JSX就相当于attributes之于HTML。从写法上来看呢,我们为组件传入props就可以像为HTML标签添加属性一样:

JSX

const SimpleButton = props =>
{/*<button style={{ color: props.color }}>Submit</button>*/}
<button className={props.color}>Submit</button>
ReactDOM.render(<SimpleButton color="blue" />, document.getElementById('root'))

HTML

<button class="blue">Submit</button>

在概念上,props对于组件就相当于JS中参数之于函数。我们可以抽象出这样一个函数来解释:

Component(props) = View

组件函数接受props作为参数最后返回视图内容。props比起原生HTML的属性要强大很多。HTML的属性只能传递某些固定的字符,props几乎可以传递所有的内容,包括变量、函数、甚至是组件本身。

props是只读的

在React中,props都是自上向下传递,从父组件传入子组件。并且props是只读的,我们不能在组件中直接修改props的内容。也即是说组件只能根据传入的props渲染界面,而不能在其内部对props进行修改。

props类型检查

正是因为props的强大,什么类型的内容都可以传递,所以在开发过程中,为了避免错误类型的内容传入,我们可以为props添加类型检查。

props默认值

由于props是只读的,我们不能直接为props赋值。React专门准备了一个方法定义props的默认值。

import React from 'react'
import PropTypes from 'prop-types'
const Title = props => <h1>{props.title}</h1>
Title.defaultProps = {
title: 'Wait for parent to pass props.'
}
Title.propTypes = {
title: PropTypes.string.isRequired
}

state

  • 初始化

  • setState方法

  • 向下传递数据

state如果要翻译的话可以翻译为状态。一个组件可以有状态,就像我们之前介绍过的有状态组件,也可以是整个React应用的状态。

在React中state也是我们进行数据交互的地方,又或者叫做state management状态管理。一个应用需要进行数据交互,比如同服务器之间的交互,同用户输入进行交互。话反过来,从API获取数据,处理用户输入也就是我们需要用到state的时候。

下面我们通过一些例子来说明state的用法以及一些需要特别注意的地方。

先看一个最简单的计数器例子,在新版本的React当中,我们通过类定义组件来声明一个有状态组件,之后在它的构造方法中初始化组件的state,我们可以先赋予它默认值。

之后就可以在组件中通过this.state来访问它,既然是state那么肯定涉及到数据的改变,因此我们还需额外定义一个负责处理state变化的函数,这样的函数中一般都会包含this.setState这个方法。

要注意到,和之前的props一样,初始化state之后,如果我们想改变它,是不可以直接对其赋值的,直接修改state的值没有任何意义,因为这样的操作脱离了React运行的逻辑,不会触发组件的重新渲染。所以需要this.setState这个方法,在改变state的同时,触发React内部的一系列函数,最后在页面上重新渲染出组件。

class Counter extends React.Component {
constructor(props) {
super(props)
this.state = {
counter: 0
}
}
addOne() {
this.setState((prevState) =>({
counter: prevState.counter + 1
}))
}
render() {
return (
<div>
<p>{ this.state.counter }</p>
<button
onClick={() => this.addOne()}>
Increment
</button>
</div>
)
}
}

根据我们上面学习到的有关props和state的知识,我们再来一起看一个稍微复杂一些的示例:

const Title = props => <h1>{props.title}</h1>
const UserInfo = props => <p>{props.firstName + ' ' + props.lastName}</p>
class LikeButton extends React.Component {
constructor(props) {
super(props)
}
handleClick() {
this.props.handleClick()
}
render() {
return <button onClick={() => this.handleClick()}>{this.props.mark}</button>
}
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = { mark: '☆' }
}
handleButtonClick() {
const mark = (this.state.mark === '★') ? '☆' : '★'
this.setState({ mark: mark })
}
render() {
return (
<div>
<Title title='Do you like this guy?' />
<UserInfo firstName='Justin' lastName='Bieber' />
<LikeButton mark={this.state.mark} handleClick={() => this.handleButtonClick()} />
</div>
)
}
}
const Root = props => <div className='container'>{props.children}</div>
ReactDOM.render(
<Root>
<App />
</Root>,
document.getElementById('root'))

同学们要注意到,在这个例子中,我依旧沿袭了上节课中展示组件和容器组件的概念。只要我们乐意,所有的React组件都可以拥有自己的state,但在实际的生产中,我们写出的90%的都是无状态组件。假如state分散在应用的各个组件当中,有的负责后端数据交互,有的负责处理用户输入,有的负责界面变换逻辑等等,后期维护起来是相当困难的。因此在开发React应用的过程中,我们应该尽量把state集中统一管理,通过props把state的数据传递到需要的组件当中。

比方说下面这个例子,在这个简易的货币汇率转换器中,3个组件,一个输入,两个输出都需要使用到同一个名为money的state,那么我们就需要为3个组件建立一个统一的父组件来管理他们公用的state,通过props将state中的值传递到子组件中,同样也可以将处理state的函数以props的方式传递下去,在子组件中触发父组件修改state的函数,以此来达到更改state重新渲染组件的目的。

class CurrencyConvertor extends React.Component {
constructor() {
super()
this.state = {
money : 100
}
}
handleInputChange(event) {
this.setState({
money: event.target.value
})
}
render() {
return (
<div>
<h2>汇率转换</h2>
<YuanInput money={this.state.money} handleChange={(e) => this.handleInputChange(e)} />
<MoneyConvertor type='美元' unit='dollar' money={this.state.money} rate={0.1453} />
<MoneyConvertor type='日元' unit='yen' money={this.state.money} rate={16.1814} />
</div>
)
}
}
class YuanInput extends React.Component {
constructor(props) {
super(props)
}
handleChange(event) {
this.props.handleChange(event)
}
render() {
return (
<p>
<label>
人民币
<input name='yuan' onChange={(e)=>this.handleChange(e)} value={this.props.money} />
</label>
</p>
)
}
}
const MoneyConvertor = props => (
<p><label>
{props.type}
<input name={props.unit} value={(props.money*props.rate).toFixed(2)} disbled />
</label></p>
)

随着我们开发应用的逐步扩展,它的state会变得越来越庞大复杂,假如分散到各个组件当中,对于日后应用的维护者来说将是一个噩梦。怎么处理怎么储存应用的state非常值得我们深入去思考,由此也就引发了一个问题——状态管理。这也正是我们学习过React之后,在下一个部分的教程中将要介绍的Redux专注解决的问题。

context

集中统一管理state也会引发另外一个问题,React当中的数据都是自上向下传递的,加入我们的state在最外层的一个组件中管理,但真正使用state中数据的组件可能在N层子组件的嵌套中,那么我们就需要在每层的子组件中都传递props下去,直至使用这个数据的子组件获取到为止,虽然可以让我们更清楚地看到数据流的传递和变化,但写起来真的非常痛苦。

React提供了一个名为context的实验性质的API来解决这一问题。我们直接来看这个对比props和context的例子:

const UserInfo = (props, context) => (
<div className="UserInfo-name">
{context.lastName + ', ' + props.firstName}
</div>
)
UserInfo.contextTypes = {
lastName: React.PropTypes.string
}
const Column = props => <div className='column'><UserInfo firstName={props.firstName} /></div>
const Row = props => <div className='row'><Column firstName={props.firstName} /></div>
const Container = props => <div className='container'><Row firstName={props.firstName} /></div>
class App extends React.Component {
constructor() {
super()
this.state = { firstName: 'Bolun'}
}
getChildContext() {
return {lastName: "Yu"}
}
render() {
return (
<div className='App'>
<Container firstName={this.state.firstName} />
</div>
)
}
}
App.childContextTypes = {
lastName: React.PropTypes.string
}

在 Codepen 上试试。

通过props我们需要传递4层才能将数据传递到我们的目标组件中,而利用context的特性,只需要在最外层的应用组件中定义,直接在目标组件中使用即可。我们通过组件中的特殊方法getChildContext来定义context,另外要注意的是,必须用childContextTypes和contextTypes各自声明context的类型,否则我们无法获取context中的数据,只会返回一个空对象。

另外在小项目(比方说 Todo/货币汇率转换器/RSS阅读器 这类数据源单一好理解好控制的这类应用我们就叫它小项目)中,你可以试着把 state 玩溜,在初具规模的项目中我们还有一些更完整的解决方案,在以后的教程当中会介绍给同学们。