写 React 组件的最佳实践

本文为译文,已得到原做者容许,原文地址:http://scottdomes.com/blog/ou...css

当我第一次开始写 React 时,我发现多少个 React 教程,就有多少种写 React 组件方法。虽然现在,框架已经成熟,可是并无一个 “正确” 写组件的方法。node

在 MuseFind 的一年以来,咱们的团队写了大量的 React 组件。咱们精益求精,不断完善写 React 组件的方法。react

本文介绍了,咱们团队写 React 组件的最佳实践。
咱们但愿,不管你是初学者,仍是经验丰富的人,这篇文章都会对你有用的。git

在开始介绍以前,先说几个点:es6

  • 咱们团队使用 ES6 和 ES7 的语法。github

  • 若是不清楚表现组件(presentational components)和容器组件(container components)之间的区别,咱们建议先阅读 这篇文章闭包

  • 若是有任何建议,问题或反馈意见,请在评论中通知咱们。框架

基于类的组件

基于类的组件(Class based components)是包含状态和方法的。
咱们应该尽量地使用基于函数的组件(Functional Components
)来代替它们。可是,如今让咱们先来说讲怎么写基于类的组件。dom

让咱们逐行地构建咱们的组件。异步

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

propTypesdefaultProps 是静态属性(static properties),在组件代码中,最好把它们写在组件靠前的位置。当其余开发人员查看这个组件的代码时,应该当即看到 propTypesdefaultProps,由于它们就好像这个组件的文档同样。(译注:关于组件书写的顺序,参考 这篇文章

若是使用 React 15.3.0 或更高版本,请使用 prop-types 代替 React.PropTypes。使用 prop-types 时,应当将其解构。

全部组件都应该有 propTypes

Methods

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'
  }
  
  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, { 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'
  }
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) => { model.name = e.target.value } 建立的新的函数也叫 闭包)。

若是将这个新函数传给一个 React 组件,不管这个组件的其余 props 有没有真正的改变,都就会致使它从新渲染。

调和(Reconciliation)是 React 中最耗费性能的一部分。所以,要避免传递新闭包的写法,不要让调和更加消耗性能!另外,传递类的方法的之中形式更容易阅读,调试和更改。

下面是咱们整个组件:

import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, 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, bool } from 'prop-types'

import './styles/Form.css'

ExpandableForm.propTypes = {
  onSubmit: func.isRequired,
  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,
  expanded: bool,
  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, bool } from 'prop-types'

import './styles/Form.css'

ExpandableForm.propTypes = {
  onSubmit: func.isRequired,
  expanded: bool,
  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>
  )
}

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,
  expanded: bool,
  onExpand: 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 Statementments),可是为了引入另外一个依赖库,咱们使用复杂的条件表达式,解决了这个问题:

图片描述

使用大括号包裹一个当即执行函数(IIFE),而后把你的 if 语句放在里面,返回你想要渲染的任何东西。
请注意,像这样的 IIFE 可能会致使一些性能消耗,但在大多数状况下,可读性更加剧要。

更新:许多评论者建议将此逻辑提取到子组件,由这些子组件返回的不一样 button。这是对的,尽量地拆分组件。

另外,当你有布尔判断渲染元素时,不该该这样作:

{
  isTrue
   ? <p>True!</p>
   : <none/>
}

应该使用短路运算:

{
  isTrue && 
    <p>True!</p>
}
相关文章
相关标签/搜索