本文为译文,已获得原作者允许,原文地址:http://scottdomes.com/blog/ou...
当我第一次开始写 React 时,我发现多少个 React 教程,就有多少种写 React 组件方法。虽然如今,框架已经成熟,但是并没有一个 “正确” 写组件的方法。
在 MuseFind 的一年以来,我们的团队写了大量的 React 组件。我们精益求精,不断完善写 React 组件的方法。
本文介绍了,我们团队写 React 组件的最佳实践。
我们希望,无论你是初学者,还是经验丰富的人,这篇文章都会对你有用的。
在开始介绍之前,先说几个点:
我们团队使用 ES6 和 ES7 的语法。
如果不清楚表现组件(presentational components)和容器组件(container components)之间的区别,我们建议先阅读 这篇文章。
基于类的组件
基于类的组件(Class based components)是包含状态和方法的。
我们应该尽可能地使用基于函数的组件(Functional Components
)来代替它们。但是,现在让我们先来讲讲怎么写基于类的组件。
让我们逐行地构建我们的组件。
引入 CSS
import React,{ Component } from 'react' import { observer } from 'mobx-react' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css'
我认为最理想的 CSS 应该是 CSS in JavaScript。但是,这仍然是一个新的想法,还没有一个成熟的解决方案出现。
所以,现在我们还是使用将 CSS 文件引入到每个 React 组件中的方法。
我们团队会先引入依赖文件(node_modules 中的文件),然后空一行,再引入本地文件。
初始化状态
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
中初始化状态的老方法。
也可以使用 ES7 这种简单的初始化状态的新方法。
更多,请阅读 这里。
propTypes and defaultProps
import React,{ Component } from 'react' import { observer } from 'mobx-react' import { string,object } from 'prop-types' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css' export default class ProfileContainer extends Component { state = { expanded: false } static propTypes = { model: object.isrequired,title: string } static defaultProps = { model: { id: 0 },title: 'Your Name' }
propTypes
和 defaultProps
是静态属性(static properties),在组件代码中,最好把它们写在组件靠前的位置。当其他开发人员查看这个组件的代码时,应该立即看到 propTypes
和 defaultProps
,因为它们就好像这个组件的文档一样。(译注:关于组件书写的顺序,参考 这篇文章)
如果使用 React 15.3.0 或更高版本,请使用 prop-types 代替 React.PropTypes。使用 prop-types
时,应当将其解构。
所有组件都应该有 propTypes
。
Methods
import React,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)
传递给子组件来实现。
我们认为,上述方法更简单,更直接。通过 ES6 箭头功能自动 bind
正确的上下文。
给 setState
传递一个函数
在上面的例子中,我们这样做:
this.setState({ expanded: !this.state.expanded })
因为 setState
它实际上是异步的。
由于性能原因,所以 React 会批量的更新状态,因此调用 setState
后状态可能不会立即更改。
这意味着在调用 setState
时,不应该依赖当前状态,因为你不能确定该状态是什么!
解决方案是:给 setState
传递函数,而不是一个普通对象。函数的第一个参数是前一个状态。
this.setState(prevState => ({ expanded: !prevState.expanded }))
解构 Props
import React,object } from 'prop-types' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css' export default class ProfileContainer extends Component { state = { expanded: false } static propTypes = { model: object.isrequired,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> ) } }
如上,当组件具有多个 props
值时,每个 prop
应当单独占据一行。
装饰器
@observer export default class ProfileContainer extends Component {
如果使用 mobx
,那么应当是用装饰器(decorators)。其本质是将装饰器的组件传递到一个函数。
使用装饰器一种更加灵活和更加可读的方式。
我们团队在使用 mobx
和我们自己的 mobx-models
库时,使用了大量的装饰器。
如果您不想使用装饰器,也可以按照下面的方式做:
class ProfileContainer extends Component { // Component code } export default observer(ProfileContainer)
闭包
避免传递一个新闭包(Closures)给子组件,像下面这样:
<input type="text" value={model.name} // onChange={(e) => { model.name = e.target.value }} // ^ 上面是错误的. 使用下面的方法: onChange={this.handleChange} placeholder="Your Name"/>
为什么呢?因为每次父组件 render
时,都会创建一个新的函数,并传递其输入的 e
。
如果其输入的 e
恰巧是一个 React 组件,无论它的其他 props
有没有真正的改变,都就会导致它重新渲染。
调和(Reconciliation)是 React 中最耗费性能的一部分。因此,要避免传递新闭包的写法,不要让调和更加消耗性能!另外,传递类的方法的之中形式更容易阅读,调试和更改。
下面是我们整个组件:
import React,object } from 'prop-types' // 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: object.isrequired,title: 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> ) } }
基于函数的组件
基于函数的组件(Functional Components)是没有状态和方法的。它们是纯粹的、易读的。尽可能的使用它们。
propTypes
import React from 'react' import { observer } from 'mobx-react' import { func,bool } from 'prop-types' import './styles/Form.css' ExpandableForm.propTypes = { onSubmit: func.isrequired,expanded: bool } // Component declaration
在声明组件之前,给组件定义 propTypes
,因为这样它们可以立即被看见。
我们可以这样做,因为 JavaScript 有函数提升(function hoisting)。
解构 Props 和 defaultProps
import React from 'react' import { observer } from 'mobx-react' import { func,expanded: bool,onExpand: 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
。我们可以使用解构参数的方式:
import React from 'react' import { observer } from 'mobx-react' import { func,bool } from 'prop-types' import './styles/Form.css' ExpandableForm.propTypes = { onSubmit: func.isrequired,onExpand: 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’ 之类的错误)
避免使用函数表达式的方式来定义组件,如下:
const ExpandableForm = ({ onExpand,expanded,children }) => {
这看起来非常酷,但是在这里,通过函数表达式定义的函数却是匿名函数。
如果 Bable 没有做相关的命名配置,那么报错时,错误堆栈中不会告诉具体是哪个组件出错了,只会显示 <<anonymous>> 。这使得调试变得非常糟糕。
匿名函数也可能会导致 React 测试库 Jest 出问题。由于这些潜在的隐患,我们推荐使用函数声明,而不是函数表达式。
包裹函数
因为基于函数的组件不能使用修饰器,所以你应当将基于函数的组件当做参数,传给修饰器对应的函数:
import React from 'react' import { observer } from 'mobx-react' import { func,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)
全部的代码如下:
import React from 'react' import { observer } from 'mobx-react' import { func,bool } from 'prop-types' // 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: func.isrequired,onExpand: func.isrequired } // Destructure props like so,and use default arguments as a way of setting defaultProps function ExpandableForm({ onExpand,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 Statementments),但是为了引入另一个依赖库,我们使用复杂的条件表达式,解决了这个问题:
使用大括号包裹一个立即执行函数(IIFE),然后把你的 if
语句放在里面,返回你想要渲染的任何东西。
请注意,像这样的 IIFE 可能会导致一些性能消耗,但在大多数情况下,可读性更加重要。
更新:许多评论者建议将此逻辑提取到子组件,由这些子组件返回的不同 button
。这是对的,尽可能地拆分组件。
另外,当你有布尔判断渲染元素时,不应该这样做:
{ isTrue ? <p>True!</p> : <none/> }
应该使用短路运算:
{ isTrue && <p>True!</p> }