[关闭]
@sambodhi 2017-02-23T00:19:08.000000Z 字数 7406 阅读 1728

编写React组件的最佳实践

React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC框架,都不满意,就决定自己写一套,用来架设Instagram的网站。做出来以后,发现这套东西很好用,就在2013年5月开源了。

由于React的设计思想极其独特,属于革命性创新,性能出众,代码逻辑却非常简单。所以,越来越多的人开始关注和使用,认为它可能是将来Web开发的主流工具。

来自MuseFind的Scott Domes日前写了一篇文章,阐述了他们编写React组件的最佳实践。Scott Domes是MuseFind的前端移动开发工程师。

经作者授权,InfoQ翻译并分享本文。以下是正文:


当我第一次开始写React代码时,我记得看到过许多不同的编写组件的方法,各教程之间有很大的不同。虽然自那时以来框架已经相当成熟,但似乎还没有一个确定“正确”编写的方式。

在过去一年里,在MuseFind,我们的团队编写了很多React组件。我们已经逐渐完善了方法,直到我们满意为止。

本指南代表我们建议的最佳做法。我们认为本文对新手和老手都有所帮助。

阅读本文之前,读者需要注意以下几点:

基于类的组件

基于类的组件是有状态的,或许还包含方法。我们尽可能少地使用它们,但它们也有自己的位置。

让我们逐行构建我们的组件。

导入CSS

  1. import React, {Component} from 'react'
  2. import {observer} from 'mobx-react'
  3. import ExpandableForm from './ExpandableForm'
  4. import './styles/ProfileContainer.css'

理论上,我喜欢CSS in JavaScript。但它仍然是一个新的想法,还没有出现一个成熟的解决方案。在此之前,我们将一个CSS文件导入到每个组件。

我们还通过换行将依赖导入与本地导入分开。

初始化状态

  1. import React, {Component} from 'react'
  2. import {observer} from 'mobx-react'
  3. import ExpandableForm from './ExpandableForm'
  4. import './styles/ProfileContainer.css'
  5. export default class ProfileContainer extends Component {
  6. state = { expanded: false }

如果你使用ES6(ES7不适用),在构造函数中初始化状态。否则,使用专用于ES7的方法。更多信息在这篇文章

我们还要确保将我们的类导出为默认类。

propTypes和defaultProps

  1. import React, {Component} from 'react'
  2. import {observer} from 'mobx-react'
  3. import ExpandableForm from './ExpandableForm'
  4. import './styles/ProfileContainer.css'
  5. export default class ProfileContainer extends Component {
  6. state = { expanded: false }
  7. static propTypes = {
  8. model: React.PropTypes.object.isRequired,
  9. title: React.PropTypes.string
  10. }
  11. static defaultProps = {
  12. model: {
  13. id: 0
  14. },
  15. title: 'Your Name'
  16. }

propTypes和defaultProps是静态属性,在组件代码中声明的优先级尽可能高。由于它们作为文档,因此它们应该对其他读取文件的开发者可见。

所有的组件应该有propTypes。

方法

  1. import React, {Component} from 'react'
  2. import {observer} from 'mobx-react'
  3. import ExpandableForm from './ExpandableForm'
  4. import './styles/ProfileContainer.css'
  5. export default class ProfileContainer extends Component {
  6. state = { expanded: false }
  7. static propTypes = {
  8. model: React.PropTypes.object.isRequired,
  9. title: React.PropTypes.string
  10. }
  11. static defaultProps = {
  12. model: {
  13. id: 0
  14. },
  15. title: 'Your Name'
  16. }
  17. handleSubmit = (e) => {
  18. e.preventDefault()
  19. this.props.model.save()
  20. }
  21. handleNameChange = (e) => {
  22. this.props.model.name = e.target.value
  23. }
  24. handleExpand = (e) => {
  25. e.preventDefault()
  26. this.setState({ expanded: !this.state.expanded })
  27. }

使用类组件,当将方法传递给子组件时,必须确保它们在调用时具有正确的this。通常通过传递this.handleSubmit.bind(this)到子组件来实现。

我们认为这种方法更简洁也更容易,通过ES6的箭头函数自动保持正确的上下文。

解构props

  1. import React, {Component} from 'react'
  2. import {observer} from 'mobx-react'
  3. import ExpandableForm from './ExpandableForm'
  4. import './styles/ProfileContainer.css'
  5. export default class ProfileContainer extends Component {
  6. state = { expanded: false }
  7. static propTypes = {
  8. model: React.PropTypes.object.isRequired,
  9. title: React.PropTypes.string
  10. }
  11. static defaultProps = {
  12. model: {
  13. id: 0
  14. },
  15. title: 'Your Name'
  16. }
  17. handleSubmit = (e) => {
  18. e.preventDefault()
  19. this.props.model.save()
  20. }
  21. handleNameChange = (e) => {
  22. this.props.model.name = e.target.value
  23. }
  24. handleExpand = (e) => {
  25. e.preventDefault()
  26. this.setState(prevState => ({ expanded: !prevState.expanded }))
  27. }
  28. render() {
  29. const {
  30. model,
  31. title
  32. } = this.props
  33. return (
  34. <ExpandableForm
  35. onSubmit={this.handleSubmit}
  36. expanded={this.state.expanded}
  37. onExpand={this.handleExpand}>
  38. <div>
  39. <h1>{title}</h1>
  40. <input
  41. type="text"
  42. value={model.name}
  43. onChange={this.handleNameChange}
  44. placeholder="Your Name"/>
  45. </div>
  46. </ExpandableForm>
  47. )
  48. }
  49. }

具有多个props的组件,每个props应该占据单独一个行,如上所示。

装饰器

  1. @observer
  2. export default class ProfileContainer extends Component {

如果你使用像mobx这样的东西,你可以将类组件装饰成这样:这与将组件传递到函数相同。

装饰器通过灵活、可读的方式来修改组件功能。我们广泛地使用装饰器,配合mobx和我们自己的mobx-models库。

如果您不想使用装饰器,请执行以下操作:

  1. class ProfileContainer extends Component {
  2. // Component code
  3. }
  4. export default observer(ProfileContainer)

闭包

避免传递新的闭包到子组件,像这样:

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

这就是为什么每次父组件渲染时,创建一个新的函数并传递给输入的原因。

如果输入是React组件,这将自动触发它重新渲染,而不管它的其他props是否实际改变。

调和算法(Reconciliation)是React最耗时的部分。不要让它比所需更难!此外,传递类的方法更容易阅读、调试和更改。

这是我们的完整组件:

  1. import React, {Component} from 'react'
  2. import {observer} from 'mobx-react'
  3. // Separate local imports from dependencies
  4. import ExpandableForm from './ExpandableForm'
  5. import './styles/ProfileContainer.css'
  6. // Use decorators if needed
  7. @observer
  8. export default class ProfileContainer extends Component {
  9. state = { expanded: false }
  10. // Initialize state here (ES7) or in a constructor method (ES6)
  11. // Declare propTypes as static properties as early as possible
  12. static propTypes = {
  13. model: React.PropTypes.object.isRequired,
  14. title: React.PropTypes.string
  15. }
  16. // Default props below propTypes
  17. static defaultProps = {
  18. model: {
  19. id: 0
  20. },
  21. title: 'Your Name'
  22. }
  23. // Use fat arrow functions for methods to preserve context (this will thus be the component instance)
  24. handleSubmit = (e) => {
  25. e.preventDefault()
  26. this.props.model.save()
  27. }
  28. handleNameChange = (e) => {
  29. this.props.model.name = e.target.value
  30. }
  31. handleExpand = (e) => {
  32. e.preventDefault()
  33. this.setState(prevState => ({ expanded: !prevState.expanded }))
  34. }
  35. render() {
  36. // Destructure props for readability
  37. const {
  38. model,
  39. title
  40. } = this.props
  41. return (
  42. <ExpandableForm
  43. onSubmit={this.handleSubmit}
  44. expanded={this.state.expanded}
  45. onExpand={this.handleExpand}>
  46. // Newline props if there are more than two
  47. <div>
  48. <h1>{title}</h1>
  49. <input
  50. type="text"
  51. value={model.name}
  52. // onChange={(e) => { model.name = e.target.value }}
  53. // Avoid creating new closures in the render method- use methods like below
  54. onChange={this.handleNameChange}
  55. placeholder="Your Name"/>
  56. </div>
  57. </ExpandableForm>
  58. )
  59. }
  60. }

函数组件

这些组件没有状态、方法。它们是纯粹的,简单的。因此要尽可能经常使用它们。

propTypes

  1. import React from 'react'
  2. import {observer} from 'mobx-react'
  3. import './styles/Form.css'
  4. const expandableFormRequiredProps = {
  5. onSubmit: React.PropTypes.func.isRequired,
  6. expanded: React.PropTypes.bool
  7. }
  8. // Component declaration
  9. ExpandableForm.propTypes = expandableFormRequiredProps

这里,我们在组件声明前分配 propTypes,因此它们立即可见。在组件声明下面,我们正确地分配它们。

解构Props和defaultProps

  1. import React from 'react'
  2. import {observer} from 'mobx-react'
  3. import './styles/Form.css'
  4. const expandableFormRequiredProps = {
  5. onSubmit: React.PropTypes.func.isRequired,
  6. expanded: React.PropTypes.bool
  7. }
  8. function ExpandableForm(props) {
  9. return (
  10. <form style={props.expanded ? {height: 'auto'} : {height: 0}}>
  11. {props.children}
  12. <button onClick={props.onExpand}>Expand</button>
  13. </form>
  14. )
  15. }

我们的组件是一个函数,它的Props作为其参数。我们可以这样扩展:

  1. import React from 'react'
  2. import {observer} from 'mobx-react'
  3. import './styles/Form.css'
  4. const expandableFormRequiredProps = {
  5. onExpand: React.PropTypes.func.isRequired,
  6. expanded: React.PropTypes.bool
  7. }
  8. function ExpandableForm({ onExpand, expanded = false, children }) {
  9. return (
  10. <form style={ expanded ? { height: 'auto' } : { height: 0 } }>
  11. {children}
  12. <button onClick={onExpand}>Expand</button>
  13. </form>
  14. )
  15. }

注意,我们也可以使用默认参数作为defaultProps以高度可读的方式。如果展开没有定义的话,我们将其设置为false。(这是一个有点强迫的例子,因为它是一个布尔,但非常有用,以避免“无法读取未定义”错误与对象)。

避免使用以下ES6语法:

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

看起来很现代,但此处的函数实际上是未命名的。

如果你的Babel设置正确,这个名字的缺失不会成为一个问题:但如果不是,任何错误将<<anonymous>>中显示,对调试而言,是一个非常严重的问题。

未命名的函数也可能导致Jest(一个React测试库)的问题。由于潜在的难以理解的bug,以及并没有什么真正的好处,我们建议使用function而不是const

包装

因为你不能使用装饰器和功能组件,你只需将函数作为参数传递给它:

  1. import React from 'react'
  2. import {observer} from 'mobx-react'
  3. import './styles/Form.css'
  4. const expandableFormRequiredProps = {
  5. onExpand: React.PropTypes.func.isRequired,
  6. expanded: React.PropTypes.bool
  7. }
  8. function ExpandableForm({ onExpand, expanded = false, children }) {
  9. return (
  10. <form style={ expanded ? { height: 'auto' } : { height: 0 } }>
  11. {children}
  12. <button onClick={onExpand}>Expand</button>
  13. </form>
  14. )
  15. }
  16. ExpandableForm.propTypes = expandableFormRequiredProps
  17. export default observer(ExpandableForm)

这是我们的完整组件:

JSX条件

很可能你要做很多条件渲染。这里是你想避免的地方:

这是我在MuseFind早期写的实际代码,饶恕我吧。

不,嵌套的三元运算并不是一个好主意。

有一些库解决了这个问题(JSX控制语句),但是,而不是引入另一个依赖,我们解决了这种应对复杂条件的方法:

以上所示是重构版本。

使用花括号括起一个IIFE,然后把你的if语句置于里面,返回任何你想要的渲染。请注意,这样的IIFE可能会导致性能下降,但在大多数情况下,它不会严重到以致失去可读性。

此外,当你只想渲染一个条件上的元素,而不是这样做:

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

使用short-circuit赋值:

  1. {
  2. isTrue &&
  3. <p>True!</p>
  4. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注