React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写一套,用来架设 Instagram 的网站。做出来以后,发现这套东西很好用,就在 2013 年 5 月开源了。
由于 React 的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来 Web 开发的主流工具。
来自 MuseFind 的 Scott Domes 日前写了一篇文章,阐述了他们编写React 组件的最佳实践。Scott Domes 是MuseFind 的前端移动开发工程师。
经作者授权,InfoQ 翻译并分享本文。以下是正文:
当我第一次开始写React 代码时,我记得看到过许多不同的编写组件的方法,各教程之间有很大的不同。虽然自那时以来框架已经相当成熟,但似乎还没有一个确定“正确”编写的方式。
在过去一年里,在 MuseFind ,我们的团队编写了很多 React 组件。我们已经逐渐完善了方法,直到我们满意为止。
本指南代表我们建议的最佳做法。我们认为本文对新手和老手都有所帮助。
阅读本文之前,读者需要注意以下几点:
- 我们使用 ES6 和 ES7 语法。
- 如果你不确定展示型组件和容器组件之间的区别,我们建议你先阅读这篇文章。
- 如果您有任何建议问题或反馈,请在原文的评论区中告诉我们。
基于类的组件
基于类的组件是有状态的,或许还包含方法。我们尽可能少地使用它们,但它们也有自己的位置。
让我们逐行构建我们的组件。
导入 CSS
import React, {Component} from 'react' import {observer} from 'mobx-react' import ExpandableForm from './ExpandableForm' import './styles/ProfileContainer.css'
理论上,我喜欢 CSS in JavaScript 。但它仍然是一个新的想法,还没有出现一个成熟的解决方案。在此之前,我们将一个 CSS 文件导入到每个组件。
我们还通过换行将依赖导入与本地导入分开。
初始化状态
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 }
如果你使用 ES6(ES7 不适用),在构造函数中初始化状态。否则,使用专用于 ES7 的方法。更多信息在这篇文章。
我们还要确保将我们的类导出为默认类。
propTypes 和 defaultProps
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 是静态属性,在组件代码中声明的优先级尽可能高。由于它们作为文档,因此它们应该对其他读取文件的开发者可见。
所有的组件应该有 propTypes。
方法
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.name = e.target.value } handleExpand = (e) => { e.preventDefault() this.setState({ expanded: !this.state.expanded }) }
使用类组件,当将方法传递给子组件时,必须确保它们在调用时具有正确的 this。通常通过传递 this.handleSubmit.bind(this) 到子组件来实现。
我们认为这种方法更简洁也更容易,通过 ES6 的箭头函数自动保持正确的上下文。
解构 props
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.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={this.handleNameChange} placeholder="Your Name"/> </div> </ExpandableForm> ) } }
具有多个 props 的组件,每个 props 应该占据单独一个行,如上所示。
装饰器
@observer export default class ProfileContainer extends Component {
如果你使用像 mobx 这样的东西,你可以将类组件装饰成这样:这与将组件传递到函数相同。
装饰器通过灵活、可读的方式来修改组件功能。我们广泛地使用装饰器,配合mobx 和我们自己的 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"/>
这就是为什么每次父组件渲染时,创建一个新的函数并传递给输入的原因。
如果输入是 React 组件,这将自动触发它重新渲染,而不管它的其他 props 是否实际改变。
调和算法(Reconciliation)是 React 最耗时的部分。不要让它比所需更难!此外,传递类的方法更容易阅读、调试和更改。
这是我们的完整组件:
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
propTypes import React from 'react' import {observer} from 'mobx-react' import './styles/Form.css' const expandableFormRequiredProps = { onSubmit: React.PropTypes.func.isRequired, expanded: React.PropTypes.bool } // Component declaration ExpandableForm.propTypes = expandableFormRequiredProps
这里,我们在组件声明前分配 propTypes,因此它们立即可见。在组件声明下面,我们正确地分配它们。
解构 Props 和 defaultProps
import React from 'react' import {observer} from 'mobx-react' import './styles/Form.css' const expandableFormRequiredProps = { onSubmit: React.PropTypes.func.isRequired, expanded: React.PropTypes.bool } function ExpandableForm(props) { return ( <form style={props.expanded ? {height: 'auto'} : {height: 0}}> {props.children} <button onClick={props.onExpand}>Expand</button> </form> ) }
我们的组件是一个函数,它的 Props 作为其参数。我们可以这样扩展:
import React from 'react' import {observer} from 'mobx-react' import './styles/Form.css' const expandableFormRequiredProps = { onExpand: React.PropTypes.func.isRequired, expanded: React.PropTypes.bool } function ExpandableForm({ onExpand, expanded = false, children }) { return ( <form style={ expanded ? { height: 'auto' } : { height: 0 } }> {children} <button onClick={onExpand}>Expand</button> </form> ) }
注意,我们也可以使用默认参数作为 defaultProps 以高度可读的方式。如果展开没有定义的话,我们将其设置为 false。(这是一个有点强迫的例子,因为它是一个布尔,但非常有用,以避免“无法读取未定义”错误与对象)。
避免使用以下 ES6 语法:
const ExpandableForm = ({ onExpand, expanded, children }) => {
看起来很现代,但此处的函数实际上是未命名的。
如果你的 Babel 设置正确,这个名字的缺失不会成为一个问题:但如果不是,任何错误将 <
未命名的函数也可能导致 Jest(一个 React 测试库)的问题。由于潜在的难以理解的 bug,以及并没有什么真正的好处,我们建议使用 function 而不是 const。
包装
因为你不能使用装饰器和功能组件,你只需将函数作为参数传递给它:
import React from 'react' import {observer} from 'mobx-react' import './styles/Form.css' const expandableFormRequiredProps = { onExpand: React.PropTypes.func.isRequired, expanded: React.PropTypes.bool } function ExpandableForm({ onExpand, expanded = false, children }) { return ( <form style={ expanded ? { height: 'auto' } : { height: 0 } }> {children} <button onClick={onExpand}>Expand</button> </form> ) } ExpandableForm.propTypes = expandableFormRequiredProps export default observer(ExpandableForm)
这是我们的完整组件:
JSX 条件
很可能你要做很多条件渲染。这里是你想避免的地方:
(点击放大图像)
这是我在MuseFind 早期写的实际代码,饶恕我吧。
不,嵌套的三元运算并不是一个好主意。
有一些库解决了这个问题( JSX 控制语句),但是,而不是引入另一个依赖,我们解决了这种应对复杂条件的方法:
(点击放大图像)
以上所示是重构版本。
使用花括号括起一个 IIFE ,然后把你的 if 语句置于里面,返回任何你想要的渲染。请注意,这样的 IIFE 可能会导致性能下降,但在大多数情况下,它不会严重到以致失去可读性。
此外,当你只想渲染一个条件上的元素,而不是这样做:
{ isTrue ? <p>True!</p> : <none/> }
使用 short-circuit 赋值:
{ isTrue && <p>True!</p> }
感谢王下邀月熊对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论