[译]react组件书写最佳实践

原文 Url: https://engineering.musefind.com/our-best-practices-for-writing-react-components-dec3eb5c3fc8


自从我开始学习 React 开始, 就在不同的教程上看到了不同的创建组件的方法. 虽然 React 框架逐渐成熟, 似乎却并没有一套’正确’的组件书写规范.

MuseFind 的一年, 我们写了大量的 React 组件, 最终, 总结出了一个我们用起来很开心的方法. 希望无论是初学者还是经验丰富的人都能从中获得帮助.

开始之前有几点需要注意:

  1. 我们用 ES6 / ES7语法
  2. 如果你不晓得展示(presentational)组件与容器(container)组件的区别, 建议先看这篇 (译注: 中文翻译)
  3. 如果有建议或疑问请去原文评论

基于 Class 的组件

基于 Class 的组件是有状态或包含了一些方法的组件. 建议尽可能谨慎的去使用这种方式, 但其使用场景却是无可取代的.

让我们从头开始构建一个组件:

引入 CSS

import React, {Component} from 'react'
import {observer} from 'mobx-react'

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

我喜欢在js中关联css. [b]在每个组件中引入 css样式[/b]

在依赖引用与本地引用之间, 建议保留一个空行

初始化 state

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

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'
  }

propTypes 与 defaultProps 是静态属性, 应尽可能在组件顶端声明. 他们应该承担文档的职责并可以第一时间被其他开发者看到.

[b]所有组件都应该有 propTypes[/b]

方法

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 })
  }

对于 class 类型的组件, 必须确保方法被调用时 this 指向正确的对象. 这里经常在调用时被写成 this.handleSubmit.bind

但用 es6 的箭头函数去维护上下文正确会更加简洁.

向 setState 传递函数

在上面的例子里, 我们写了

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

这里有一个关于setState不得不说的秘密 — 他是异步的. React 处于性能考虑会异步批量更新 state, 调用 setState 后 state 不会立即变化.

这意味着你不能立即拿到改变后 state 的值. 这里有一个解决办法: 向 setState 传递一个第一个参数为 prevState 的函数

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

props 解构

// 以上代码省略

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>
  )
}

需要解构多个 props 的话, 每一个 prop 应该占一行

(译注: 关于解构 / 对象/ 数组的最后一个元素, 建议后面保留逗号, 这一行为在 es6中是被允许的, 并且会在 babel 编译后可以支持 IE8 等浏览器, 保留逗号再日后修改时, 可以不去影响上一行的东西, 利于代码合并与审查
另外组件末尾的 />前个人偏好加一个换行, 可以更加直观的找到一个组件结尾的地方)

Decorator(装饰器)

@observer
export default class ProfileContainer extends Component {

如果使用 mbox 之类的东西, 可以用这种方式装饰你的组件.

装饰器 (译注: 中文介绍)更加灵活易读. 在我们的项目中, 有大量使用装饰器, mobx 及我们自己的 (mbox模块)[https://github.com/musefind/mobx-models]

如果你不想用装饰器, 可以写成下面这种方式

class ProfileContainer extends Component {
  // Component code
}
export default observer(ProfileContainer)

闭包

不要在子组件中传递闭包

<input
  type="text"
  value={model.name}
  // onChange={(e) => { model.name = e.target.value }}
  // ^ Not this. Use the below:
  onChange={this.handleChange}
  placeholder="Your Name"/>

否则, 每一次父组件渲染都会生成一个新的函数传递给 input.

如果 input 是一个 react 组件, 这将导致无论其他 props 有无改变都会触发重新渲染.

一致性(Reconciliation) 是 React 中开销最高的一环, 不要让这一步更加复杂. 另外, 传递一个方法也更利于阅读, 调试和改动.

以下是我们完整的组件 (gist)

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'
// Separate local imports from dependencies
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

// Use decorators if needed
@observer
export default class ProfileContainer extends Component {
state = { expanded: false }
// Initialize state here (ES7) or in a constructor method (ES6)

// Declare propTypes as static properties as early as possible
static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}

// Default props below propTypes
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}

// Use fat arrow functions for methods to preserve context (this will thus be the component instance)
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() {
// Destructure props for readability
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 }}
// Avoid creating new closures in the render method- use methods like below
onChange={this.handleNameChange}
placeholder="Your Name"/>
</div>
</ExpandableForm>
)
}
}

基于函数的组件

没有状态和方法的组件是更易理解纯组件. (译注: 个人认为”没有方法”这一限制条件是多余的) 纯组件.

应该尽可能的使用纯组件.

propTypes

import React from 'react'
import {observer} from 'mobx-react'

import './styles/Form.css'

ExpandableForm.propTypes = {
  onSubmit: React.PropTypes.func.isRequired,
  expanded: React.PropTypes.bool
}
// Component declaration

我们在组件声明前就定义了 proTypes, 使之可以第一时间被开发人员看见. 这里利用了JavaScript 里函数声明提前的原理

props 解构 和 defaultProps

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, 可以扩展成像这样:

function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {

注意, 这里也可以把 defaultProps在函数声明前定义. 如果参数 expanded不存在, 则设为 false.(这个例子有点牵强, undefined 的布尔值也是 false, 但是在某些会造成 Cannot read <property> of undefined 异常的情况下就很有用了)

避免以下的 ES6语法

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

看起来很高端, 但是这个函数实际上是匿名的. 这对 babel 编译没有影响, 但如果内部有错误在控制台中看到的错误来源会是 <<anonymous>>, 非常不利于调试.

匿名函数在 Jest 中也会造成问题, 我们推荐使用 function 替代 const

包裹(Wrapping)

鉴于不能用装饰器装饰一个函数, 这里只简单的对函数进行了一次包裹

export default observer(ExpandableForm)

完整的代码如下 gist

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'
// Separate local imports from dependencies
import './styles/Form.css'

// Declare propTypes here, before the component (taking advantage of JS function hoisting)
// You want these to be as visible as possible
ExpandableForm.propTypes = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool,
onExpand: React.PropTypes.func.isRequired
}

// Destructure props like so, and use default arguments as a way of setting defaultProps
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>
)
}

// Wrap the component instead of decorating it
export default observer(ExpandableForm)

jsx 条件

应该尽可能避免以下这种写法:

嵌套三元运算符不是一个好主意.

有一些类库可以解决这个问题(JSX-Control Statements). 如果不想引入一个新依赖, 我们还可以写成这样:

用花括号包裹一个 IIFE, 并把 if 条件语句写在其中, 返回想要渲染的内容. 像这样的 IIFE 可以优化性能, 但往往并不见得比牺牲掉的可读性更划算.

许多评论(译注: 当然是原文的评论)更倾向于将条件的逻辑放入一个子组件中. 这种说法是正确的, 尽可能的拆分组件总是好的, 但 IIFE 也不失为退一步的方法.