寫 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>
}
相關文章
相關標籤/搜索