0%

React组件最佳实践

** 文章是翻译自Medium,原文链接:Our Best Practices for Writing React Components **


我开始写React的时候就发现写组件有很多不同的方法,教程与教程之间的差别很大。尽管框架与那时相比已经完全成熟了,但是到目前还没有出现一个的被所有人都接受的“正确”书写方式。
在过去的几年里,我们团队写了大量的React组件,我们不断优化书写组件的方案,直到我们满意为止。这个指导代表了我们推荐的最佳实践,我们希望这能对新手和老手都能有帮助。
在开始之前,还有几点要说明的:

  • 我们使用了ES6和ES7语法。
  • 如果你还不了解presentational组件和container组件的区别,建议你先去看看这个。
  • 如果有什么建议、疑问或者反馈,请在评论区告诉我们。

基于类的组件

基于类的组件具有状态,通常会有一些方法,我们应该尽量少地使用基于类的组件,但它们有它们的应用场景。

接下来我们一行一行地构建我们的应用。

引入CSS

1
2
3
4
5
import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

我非常喜欢CSS in JavaScript的理念,但这到目前为止仍然是一个相对比较新鲜的想法,也没有一个成熟的解决方案,所以在那之前,我们为每个组件都导入一个CSS文件。
我们还会使用一个空行来开区分引入的dependency和从本地引入的内容。

初始化状态

1
2
3
4
5
6
7
8
import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {
state = { expanded: false }

你也可以使用像这样的方法在constructor里初始化状态,我们倾向于使用更简单清晰的方式。
我们也会保证默认导出我们的class。

propTypes和defaultProps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {
state = { expanded: false }

static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}

static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}

propTypesdefaultProps是静态特性,在组件内声明越靠上越好,应该确保其他开发者在读这个文件时立即读到这部分,因为他们的作用相当于是文档。
所有的组件都应该有propTypes

方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {
state = { expanded: false }

static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}

static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}

handleSubmit = (e) => {
e.preventDefault()
this.props.model.save()
}

handleNameChange = (e) => {
this.props.model.changeName(e.target.value)
}

handleExpand = (e) => {
e.preventDefault()
this.setState({ expanded: !this.state.expanded })
}

基于类的组件中当需要向子组件传递方法时,你必须保证他们被调用时的this的指向是正确的。通常人们会通过传递例如this.handleSubmit.bind(this)大方法来确保子组件的方法会得到正确的this
我们建议通过ES6的箭头函数来使子组件自动获取到正确的上下文,这种方法会更清晰、更便捷一些。

给setState传一个函数

在上面的例子中,我们使这样做的:

1
this.setState({ expanded: !this.state.expanded })

关于setState有一个比较恶心的地方——它其实是异步的。React出于对性能方面的考虑,会把一系列state的变化打包然后一起执行,所以state有可能并不会在setState调用后就立即发生变化,这也就意味着当你调用setState时你就不该指着当前的状态,因为你并不能确认这个state是什么样子的。
解决方案就是——给setState传将前一个状态作为参数的函数。

1
this.setState(prevState => ({ expanded: !prevState.expanded }))

(感谢Austin Wood对这部分的帮忙)。

解构props

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import React, {Component} from 'react'
import {observer} from 'mobx-react'

import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

export default class ProfileContainer extends Component {
state = { expanded: false }

static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}

static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
handleSubmit = (e) => {
e.preventDefault()
this.props.model.save()
}

handleNameChange = (e) => {
this.props.model.changeName(e.target.value)
}

handleExpand = (e) => {
e.preventDefault()
this.setState(prevState => ({ expanded: !prevState.expanded }))
}

render() {
const {
model,
title
} = this.props
return (
<ExpandableForm
onSubmit={this.handleSubmit}
expanded={this.state.expanded}
onExpand={this.handleExpand}>
<div>
<h1>{title}</h1>
<input
type="text"
value={model.name}
onChange={this.handleNameChange}
placeholder="Your Name"/>
</div>
</ExpandableForm>
)
}
}

如果组件有多个组件的话,如上所示,应该把每个prop都写在单独的一行。

修饰器

1
2
@observer
export default class ProfileContainer extends Component {

如果使用了类似mobx状态管理库,你可以像上面这样修饰组件——把组件传递给一个函数的效果是一样的。

通过修饰器是来修改组件的功能非常灵活并且有很好的可读性,配合着mobx和我们自己的mobx-models库,我们大量使用了修饰器。当然如果你愿意使用修饰器,可以按照下面的写法去做:

1
2
3
4
5
class ProfileContainer extends Component {
// 组件代码
}

export default observer(ProfileContainer)

闭包

应当避免将新的闭包传递给子组件:

1
2
3
4
5
6
7
<input
type="text"
value={model.name}
// onChange={(e) => { model.name = e.target.value }}
// ^ 不要使用上面的这种方法,使用下面的:
onChange={this.handleChange}
placeholder="Your Name"/>

这是因为每次父组件渲染的时候,都会产生一个新的函数然后赋给input。如果input是一个React组件,这就会导致它不管其他的props是否发生变化都会自动重新渲染。

一致性是React最有价值的部分之一,不要将React搞到不必要的复杂程度,此外,传递一个类的方法会更容易阅读、调试和更改。

以下是我们的完整的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import React, {Component} from 'react'
import {observer} from 'mobx-react'
// 依赖项和本地的引入分隔开来
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

// 如果有需要的话使用修饰器
@observer
export default class ProfileContainer extends Component {
state = { expanded: false }
// 在这里直接初始化state(ES7)或者在一个constructor方法中(ES6)初始化

// 尽早地以静态属性声明propTypes
static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}

// 在propTypes下声明默认属性值
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}

// 方法使用胖箭头函数来保留上下文(this的值因此会指向组件实例)
handleSubmit = (e) => {
e.preventDefault()
this.props.model.save()
}

handleNameChange = (e) => {
this.props.model.name = e.target.value
}

handleExpand = (e) => {
e.preventDefault()
this.setState(prevState => ({ expanded: !prevState.expanded }))
}

render() {
// 处于可读性考虑,解构props
const {
model,
title
} = this.props
return (
<ExpandableForm
onSubmit={this.handleSubmit}
expanded={this.state.expanded}
onExpand={this.handleExpand}>
// Newline props if there are more than two
<div>
<h1>{title}</h1>
<input
type="text"
value={model.name}
// onChange={(e) => { model.name = e.target.value }}
// 避免在render方法中创建新的闭包,使用下面的形式来传递方法
onChange={this.handleNameChange}
placeholder="Your Name"/>
</div>
</ExpandableForm>
)
}
}

函数定义的组件

这类的组价没有state,也没有方法,他们是纯净的,很容易判断出结果,我们应该尽可能多地使用这类型的组件。

propTypes

1
2
3
4
5
6
7
8
9
10
import React from 'react'
import {observer} from 'mobx-react'

import './styles/Form.css'

ExpandableForm.propTypes = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool
}
// 组件申明

在这里我们在声明组件之前就定义propTypes,以便我们第一眼就能看到,我们可以这样做是因为函数声明会提升。

解构Props和defaultProps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react'
import {observer} from 'mobx-react'

import './styles/Form.css'

ExpandableForm.propTypes = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool,
onExpand: React.PropTypes.func.isRequired
}

function ExpandableForm(props) {
const formStyle = props.expanded ? {height: 'auto'} : {height: 0}
return (
<form style={formStyle} onSubmit={props.onSubmit}>
{props.children}
<button onClick={props.onExpand}>Expand</button>
</form>
)
}

我们的组件是一个接收props作为参数的函数,还可以进一步改写成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react'
import {observer} from 'mobx-react'

import './styles/Form.css'

ExpandableForm.propTypes = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool,
onExpand: React.PropTypes.func.isRequired
}

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
const formStyle = expanded ? {height: 'auto'} : {height: 0}
return (
<form style={formStyle} onSubmit={onSubmit}>
{children}
<button onClick={onExpand}>Expand</button>
</form>
)
}

请注意我们可以使用函数的默认参数值来定义defaultProps,这样的话可读性更高。在这里的如果expanded是未定义的,我们可以把它设置为false(这里只是一个布尔值,看起来可能有为了举例而举例的嫌疑,但是对于避免出现Cannot read <property> of undefined的报错可是非常有用)。

还应该避免使用下面的ES6语法:

1
const ExpandableForm = ({ onExpand, expanded, children }) => {

看起来很酷炫,但是这里的函数其实还是未命名的。在正确配置了Babel的情况下,这里缺函数名不会有什么问题,但是如果Babel没有配置好,所有的错误都显示出现在<< anonymous >>函数中,这样的话,调试就会异常恐怖,此外,匿名函数在使用React测试库Jest的时候也会有可能出现问题。鉴于有造成难以理解的BUG的倾向(而且并没有什么优点),所以我们推荐使用function,不用使用const

Wrapping

由于在函数定义的组件中不能使用修饰器,那么可以把这个函数当做一个参数来传给observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React from 'react'
import {observer} from 'mobx-react'

import './styles/Form.css'

ExpandableForm.propTypes = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool,
onExpand: React.PropTypes.func.isRequired
}

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
const formStyle = expanded ? {height: 'auto'} : {height: 0}
return (
<form style={formStyle} onSubmit={onSubmit}>
{children}
<button onClick={onExpand}>Expand</button>
</form>
)
}

export default observer(ExpandableForm)

以下是我们的完整的函数定义的组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import React from 'react'
import {observer} from 'mobx-react'
// 使用空行将引入依赖项和引入本地内容分隔开来
import './styles/Form.css'

// 在组件之前声明propTypes,利用了JS的函数声明提升
// 这些声明越容易被看到越好
ExpandableForm.propTypes = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool,
onExpand: React.PropTypes.func.isRequired
}

// 像下面这样对props进行解构并且可以把函数的默认参数值当做一种设置默认属性值的方法
function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
const formStyle = expanded ? { height: 'auto' } : { height: 0 }
return (
<form style={formStyle} onSubmit={onSubmit}>
{children}
<button onClick={onExpand}>Expand</button>
</form>
)
}

// 不要修饰组件,而是将组件包裹起来
export default observer(ExpandableForm)

JSX中的条件句

不出什么意外的话,你肯定会做很多的条件渲染工作,是不是想极力避免如下图这样的情形?

一定是的,嵌套的三目运算简直是个噩梦。

有一些解决这个问题的库(如JSX-Control Statements),但是与其再引入一个依赖项,我们决定使用下面的方法来解决问题:

使用IIFE,在里面写if语句然后返回任何你想要渲染的内容。要注意这样使用IIFE会造成一定的性能的损耗,但是在绝大多数情况下跟可读性比起来,这点儿性能损耗就显得无足轻重了。

更新:许多人评论说这里的条件渲染建议改写成子组件然后通过props来返回不同的按钮。他们说的是对的,尽可能地将组建拆分成小的组件是正确的选择,但是将IIFE方法记在心上,可以当做备用的方法。

此外,如果你只想在某个特定的情形下渲染一个组件,不要这样做:

1
2
3
4
5
{
isTrue
? <p>True!</p>
: <none/>
}

而是应该使用&&

1
2
3
4
{
isTrue &&
<p>True!</p>
}

结尾

Bla……Bla……