編寫React組件的最佳實(shí)踐

此文翻譯自這里

當(dāng)我剛開(kāi)始寫React的時(shí)候警检,我看過(guò)很多寫組件的方法。一百篇教程就有一百種寫法害淤。雖然React本身已經(jīng)成熟了扇雕,但是如何使用它似乎還沒(méi)有一個(gè)“正確”的方法。所以我(作者)把我們團(tuán)隊(duì)這些年來(lái)總結(jié)的使用React的經(jīng)驗(yàn)總結(jié)在這里窥摄。希望這篇文字對(duì)你有用镶奉,不管你是初學(xué)者還是老手。

開(kāi)始前:

  • 我們使用ES6崭放、ES7語(yǔ)法
  • 如果你不是很清楚展示組件和容器組件的區(qū)別哨苛,建議您從閱讀這篇文章開(kāi)始
  • 如果您有任何的建議、疑問(wèn)都清在評(píng)論里留言

基于類的組件

現(xiàn)在開(kāi)發(fā)React組件一般都用的是基于類的組件币砂。下面我們就來(lái)一行一樣的編寫我們的組件:

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

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

我很喜歡css in javascript建峭。但是,這個(gè)寫樣式的方法還是太新了决摧。所以我們?cè)诿總€(gè)組件里引入css文件亿蒸。而且本地引入的import和全局的import會(huì)用一個(gè)空行來(lái)分割。

初始化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掌桩。更多相關(guān)可以看這里边锁。但是我們選擇更加清晰的方法。
同時(shí)波岛,我們確保在類前面加上了export default茅坛。(譯者注:雖然這個(gè)在使用了redux的時(shí)候不一定對(duì))。

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是靜態(tài)屬性则拷。盡可能在組件類的的前面定義贡蓖,讓其他的開(kāi)發(fā)人員讀代碼的時(shí)候可以立刻注意到祟剔。他們可以起到文檔的作用。

如果你使用了React 15.3.0或者更高的版本摩梧,那么需要另外引入prop-types包,而不是使用React.PropTypes宣旱。更多內(nèi)容移步這里仅父。

你所有的組件都應(yīng)該有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 })
  }

  // ...

}

在類組件里浑吟,當(dāng)你把方法傳遞給子組件的時(shí)候笙纤,需要確保他們被調(diào)用的時(shí)候使用的是正確的this。一般都會(huì)在傳給子組件的時(shí)候這么做:this.handleSubmit.bind(this)组力。

使用ES6的箭頭方法就簡(jiǎn)單多了省容。它會(huì)自動(dòng)維護(hù)正確的上下文(this)。

給setState傳入一個(gè)方法

在上面的例子里有這么一行:

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

setState其實(shí)是異步的燎字!React為了提高性能腥椒,會(huì)把多次調(diào)用的setState放在一起調(diào)用。所以候衍,調(diào)用了setState之后state不一定會(huì)立刻就發(fā)生改變笼蛛。

所以,調(diào)用setState的時(shí)候蛉鹿,你不能依賴于當(dāng)前的state值滨砍。因?yàn)閕根本不知道它是值會(huì)是神馬。

解決方法:給setState傳入一個(gè)方法妖异,把調(diào)用前的state值作為參數(shù)傳入這個(gè)方法惋戏。看看例子:

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的响逢,每一個(gè)prop都應(yīng)該單獨(dú)占一行。就如上例一樣矩乐。要達(dá)到這個(gè)目標(biāo)最好的方法是使用一套工具:Prettier龄句。

裝飾器(Decorator)

@observer
export default class ProfileContainer extends Component {

如果你了解某些庫(kù),比如mobx散罕,你就可以使用上例的方式來(lái)修飾類組件分歇。裝飾器就是把類組件作為一個(gè)參數(shù)傳入了一個(gè)方法。

裝飾器可以編寫更靈活欧漱、更有可讀性的組件职抡。如果你不想用裝飾器,你可以這樣:

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是一個(gè)React組件的話缚甩,這樣自動(dòng)觸發(fā)它的重繪谱净,不管其他的props是否發(fā)生了改變。

一致性檢驗(yàn)是React最消耗資源的部分擅威。不要把額外的工作加到這里壕探。處理上例中的問(wèn)題最好的方法是傳入一個(gè)類方法,這樣還會(huì)更加易讀郊丛,更容易調(diào)試李请。如:

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

方法組件

這類組件沒(méi)有state沒(méi)有props,也沒(méi)有方法厉熟。它們是純組件导盅,包含了最少的引起變化的內(nèi)容。經(jīng)常使用它們揍瑟。

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

我們?cè)诮M件的聲明之前就定義了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>
  )
}

我們的組件是一個(gè)方法。它的參數(shù)就是props绢片。我們可以這樣擴(kuò)展這個(gè)組件:

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

現(xiàn)在我們也可以使用默認(rèn)參數(shù)來(lái)扮演默認(rèn)props的角色滤馍,這樣有很好的可讀性。如果expanded沒(méi)有定義杉畜,那么我們就把它設(shè)置為false纪蜒。

但是,盡量避免使用如下的例子:

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

看起來(lái)很現(xiàn)代此叠,但是這個(gè)方法是未命名的纯续。

如果你的Babel配置正確,未命名的方法并不會(huì)是什么大問(wèn)題灭袁。但是猬错,如果Babel有問(wèn)題的話,那么這個(gè)組件里的任何錯(cuò)誤都顯示為發(fā)生在 <<anonymous>>里的茸歧,這調(diào)試起來(lái)就非常麻煩了倦炒。

匿名方法也會(huì)引起Jest其他的問(wèn)題。由于會(huì)引起各種難以理解的問(wèn)題软瞎,而且也沒(méi)有什么實(shí)際的好處逢唤。我們推薦使用function,少使用const涤浇。

裝飾方法組件

由于方法組件沒(méi)法使用裝飾器掩驱,只能把它作為參數(shù)傳入別的方法里扭弧。

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)

條件判斷

某些情況下洒嗤,你會(huì)做很多的條件判斷:

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

這么多層的條件判斷可不是什么好現(xiàn)象。

有第三方庫(kù)JSX-Control Statements可以解決這個(gè)問(wèn)題。但是與其增加一個(gè)依賴喉誊,還不如這樣來(lái)解決:

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

使用大括號(hào)包起來(lái)的IIFE邀摆,然后把你的if表達(dá)式都放進(jìn)去。返回你要返回的組件伍茄。

最后

再次栋盹,希望本文對(duì)你有用。如果你有什么好的意見(jiàn)或者建議的話請(qǐng)寫在下面的評(píng)論里敷矫。謝謝贞盯!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市沪饺,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌闷愤,老刑警劉巖整葡,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異讥脐,居然都是意外死亡遭居,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門旬渠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)俱萍,“玉大人,你說(shuō)我怎么就攤上這事告丢∏鼓ⅲ” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵岖免,是天一觀的道長(zhǎng)岳颇。 經(jīng)常有香客問(wèn)我,道長(zhǎng)颅湘,這世上最難降的妖魔是什么话侧? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮闯参,結(jié)果婚禮上瞻鹏,老公的妹妹穿的比我還像新娘。我一直安慰自己鹿寨,他們只是感情好新博,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著释移,像睡著了一般叭披。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,708評(píng)論 1 305
  • 那天涩蜘,我揣著相機(jī)與錄音嚼贡,去河邊找鬼。 笑死同诫,一個(gè)胖子當(dāng)著我的面吹牛粤策,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播误窖,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼叮盘,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了霹俺?” 一聲冷哼從身側(cè)響起柔吼,我...
    開(kāi)封第一講書(shū)人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丙唧,沒(méi)想到半個(gè)月后愈魏,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡想际,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年培漏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胡本。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡牌柄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出侧甫,到底是詐尸還是另有隱情珊佣,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布披粟,位于F島的核電站彩扔,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏僻爽。R本人自食惡果不足惜虫碉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望胸梆。 院中可真熱鬧敦捧,春花似錦、人聲如沸碰镜。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)绪颖。三九已至秽荤,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背窃款。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工课兄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人晨继。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓烟阐,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親紊扬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蜒茄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容