React组件最佳实践

原文:Our Best Practices for Writing React Components

作者:Scott Domes

当我第一次开始写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 }

你也可以在constructor中初始化state,更多相关内容查看reactjs - React constructor ES6 vs ES7 - Stack Overflow。我们喜欢更简洁的方法。我们还确保把类输出为默认值。

propTypes 和 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是组件中的静态属性,应该尽可能在代码顶部声明。它们提高了代码的可读性,起着文档的作用。

如果使用React 15.3.0或者更高的版本,使用prop-types代替。PropTypes毫无疑问可以让代码更好的结构化。

应该在所有的组件中都使用propTypes。

方法

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的箭头函数来自动维护正确的作用域。

给setState传递函数

在上面的例子中,我们这么写:

this.setState({ expanded: !this.state.expanded })

这是setState的脏秘诀—它实际上是异步的。出于性能的原因React整合state变化后一起处理,所以state不一定在setState调用之后直接改变。

这就意味着你在调用setState时不能依赖当前的state值,因为你不能确定state是什么样的。

解决方法是 — 传递一个函数给setState,并把之前的state作为参数。

this.setState(prevState => ({ expanded: !prevState.expanded }))

(感谢Austin Wood对本小节内容的帮助)

解构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>
    )
  }
}

有很多属性的组件,应该解构并让每个属性单独占用一行,像上面那样。

Decorators修饰器

@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 }}
  // ^ 不要这样. 使用下面的写法:
  onChange={this.handleChange}
  placeholder="Your Name"/>

理由是:每当父组件渲染的时候,都会创建一个新的函数并传给input。

如果input是一个React组件,这必然会自动触发他的re-render,无论它的其余属性是否真的发生改变。

一致性对比是React中消耗最大的部分。不要让它变得比它本需要的更加复杂!另外,传递一个方法也更容易阅读、调试和变更。

这里是我们完整的组件:

import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'
// 把本地文件引入和依赖区分开
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'

// 如果需要的话使用修饰器
@observer
export default class ProfileContainer extends Component {
  state = { expanded: false }
  // 初始化state(ES7) 或者在constructor中初始化state (ES6)
 
  // 将propTypes简单的定义为静态属性
  static propTypes = {
    model: object.isRequired,
    title: string
  }

  // 在propTypes后面写defaultProps
  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>
    )
  }

函数组件

这类组件没有状态和方法。它们是纯粹的,并且易于理解。尽可能多的使用它们。

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
}
// 组件声明

这里,我们在组件声明前赋值propTypes,很直观。我们可以这么做是因为JavaScript的函数提升机制。

解构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
}

// 译注:es6函数默认参数和函数参数的解构
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。如果被展开项是undefined,我们就将它设置为false。(避免了对象‘不能读取未定义的属性’错误)

避免下面的ES6语法:

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

看着很高级,但是这里的函数是匿名的。

如果你的Babel设置正确这并不会造成问题,但是如果没有,任何错误都会显示成由<<anonymous>>引起,给调试工作带来严重困扰。

匿名函数也会给React的一个测试库Jest带来问题。由于潜在的调试困难(并且这么写也没有任何好处),我们推荐使用function而不是const

Wrapping

由于函数组件不能使用修饰器,你可以简单的把它作为参数传递给函数:

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'
// 把本地文件引入和(第三方)依赖区分开
import './styles/Form.css'

// 在这里声明propTypes, 在组件之前( JS 函数提升)
// 你想让这些尽可能的可见
ExpandableForm.propTypes = {
  onSubmit: func.isRequired,
  expanded: bool,
  onExpand: func.isRequired
}

// 像这样解构props、使用函数默认参数来设置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>
  )
}

// 取代修饰器那样的写法
export default observer(ExpandableForm)

JSX中的条件语句

写组件时候可能会做很多逻辑判断。这种写法要避免:


嵌套的三元操作符不是好的想法。

有一些库可以解决这个问题JSX Control Statements,但是比着引入其他的依赖,我们用这种方法解决复杂的条件选择:

使用花括号包裹一个IIFE(立即执行函数),然后放入if语句,返回你想渲染的东西。注意这样的IIFE可能会引起一些性能问题,但是大部分情况下并不足以成为降低可读性的理由。

更新:很多评论者推荐把这里的逻辑处理成基于props有条件的返回不同的buttons的子组件。他们是正确的 — 尽可能的细化组件是好主意。但是请把这种IIFE方法作为条件渲染的备选方案。

当你只想在某个条件下渲染一个元素的时候,不要这么写:

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

用更简单的写法代替:

{
  isTrue && 
    <p>True!</p>
}

结论

(译注:这里是medium里留言点赞收藏之类的就不写了)

Comments
Write a Comment