編寫React組件的最佳實踐

此文翻譯自這裏javascript

當我剛開始寫React的時候,我看過不少寫組件的方法。一百篇教程就有一百種寫法。雖然React自己已經成熟了,可是如何使用它彷佛尚未一個「正確」的方法。因此我(做者)把咱們團隊這些年來總結的使用React的經驗總結在這裏。但願這篇文字對你有用,無論你是初學者仍是老手。css

開始前:java

  • 咱們使用ES六、ES7語法
  • 若是你不是很清楚展現組件和容器組件的區別,建議您從閱讀這篇文章開始
  • 若是您有任何的建議、疑問都清在評論裏留言

基於類的組件

如今開發React組件通常都用的是基於類的組件。下面咱們就來一行同樣的編寫咱們的組件:react

import React, { Component } from 'react';
import { observer } from 'mobx-react';

import ExpandableForm from './ExpandableForm';
import './styles/ProfileContainer.css';

我很喜歡css in javascript。可是,這個寫樣式的方法仍是太新了。因此咱們在每一個組件裏引入css文件。並且本地引入的import和全局的import會用一個空行來分割。git

初始化State

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。更多相關能夠看這裏。可是咱們選擇更加清晰的方法。
同時,咱們確保在類前面加上了export default。(譯者注:雖然這個在使用了redux的時候不必定對)。es6

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是靜態屬性。儘量在組件類的的前面定義,讓其餘的開發人員讀代碼的時候能夠馬上注意到。他們能夠起到文檔的做用。github

若是你使用了React 15.3.0或者更高的版本,那麼須要另外引入prop-types包,而不是使用React.PropTypes。更多內容移步這裏redux

你全部的組件都應該有prop types閉包

方法

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)app

使用ES6的箭頭方法就簡單多了。它會自動維護正確的上下文(this)。

給setState傳入一個方法

在上面的例子裏有這麼一行:

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

setState實際上是異步的!React爲了提升性能,會把屢次調用的setState放在一塊兒調用。因此,調用了setState以後state不必定會馬上就發生改變。

因此,調用setState的時候,你不能依賴於當前的state值。由於i根本不知道它是值會是神馬。

解決方法:給setState傳入一個方法,把調用前的state值做爲參數傳入這個方法。看看例子:

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

感謝Austin Wood的幫助。

拆解組件

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都應該單獨佔一行。就如上例同樣。要達到這個目標最好的方法是使用一套工具:Prettier

裝飾器(Decorator)

@observer
export default class ProfileContainer extends Component {

若是你瞭解某些庫,好比mobx,你就可使用上例的方式來修飾類組件。裝飾器就是把類組件做爲一個參數傳入了一個方法。

裝飾器能夠編寫更靈活、更有可讀性的組件。若是你不想用裝飾器,你能夠這樣:

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"/>

注意:若是input是一個React組件的話,這樣自動觸發它的重繪,無論其餘的props是否發生了改變。

一致性檢驗是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>
    )
  }
}

方法組件

這類組件沒有state沒有props,也沒有方法。它們是純組件,包含了最少的引發變化的內容。常用它們。

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

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

如今咱們也可使用默認參數來扮演默認props的角色,這樣有很好的可讀性。若是expanded沒有定義,那麼咱們就把它設置爲false

可是,儘可能避免使用以下的例子:

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

看起來很現代,可是這個方法是未命名的。

若是你的Babel配置正確,未命名的方法並不會是什麼大問題。可是,若是Babel有問題的話,那麼這個組件裏的任何錯誤都顯示爲發生在 < >裏的,這調試起來就很是麻煩了。

匿名方法也會引發Jest其餘的問題。因爲會引發各類難以理解的問題,並且也沒有什麼實際的好處。咱們推薦使用function,少使用const

裝飾方法組件

因爲方法組件無法使用裝飾器,只能把它做爲參數傳入別的方法裏。

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)

只能這樣處理: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)

條件判斷

某些狀況下,你會作不少的條件判斷:

<div id="lb-footer">
  {props.downloadMode && currentImage && !currentImage.video && currentImage.blogText
  ? !currentImage.submitted && !currentImage.posted
  ? <p>Please contact us for content usage</p>
    : currentImage && currentImage.selected
      ? <button onClick={props.onSelectImage} className="btn btn-selected">Deselect</button>
      : currentImage && currentImage.submitted
        ? <button className="btn btn-submitted" disabled>Submitted</button>
        : currentImage && currentImage.posted
          ? <button className="btn btn-posted" disabled>Posted</button>
          : <button onClick={props.onSelectImage} className="btn btn-unselected">Select post</button>
  }
</div>

這麼多層的條件判斷可不是什麼好現象。

有第三方庫JSX-Control Statements能夠解決這個問題。可是與其增長一個依賴,還不如這樣來解決:

<div id="lb-footer">
  {
    (() => {
      if(downloadMode && !videoSrc) {
        if(isApproved && isPosted) {
          return <p>Right click image and select "Save Image As.." to download</p>
        } else {
          return <p>Please contact us for content usage</p>
        }
      }

      // ...
    })()
  }
</div>

使用大括號包起來的IIFE,而後把你的if表達式都放進去。返回你要返回的組件。

最後

再次,但願本文對你有用。若是你有什麼好的意見或者建議的話請寫在下面的評論裏。謝謝!

相關文章
相關標籤/搜索