** 文章是翻译自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' }
propTypes
和defaultProps
是静态特性,在组件内声明越靠上越好,应该确保其他开发者在读这个文件时立即读到这部分,因为他们的作用相当于是文档。 所有的组件都应该有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={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 } 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.name = 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={(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' 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)
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……