通過(guò)實(shí)例赴精,學(xué)習(xí)編寫 React 組件的“最佳實(shí)踐”

Let's React

現(xiàn)在前端程序員都知道,React 是組件化的绞幌。當(dāng)我開始學(xué)習(xí) React 的時(shí)候蕾哟,我記得當(dāng)時(shí)已經(jīng)存在了很多不同編寫組件的方式了。如今莲蜘,React
社區(qū)已經(jīng)愈發(fā)成熟谭确,但是對(duì)于組件正確編寫姿勢(shì)卻沒(méi)有一個(gè)相對(duì)完備的指導(dǎo)。

這篇文章僅從作者的觀點(diǎn)出發(fā)票渠,來(lái)談一談我們究竟應(yīng)該如何來(lái)寫高質(zhì)量的 React 組件逐哈。

在開始前,需要說(shuō)明以下幾個(gè)問(wèn)題:

  • 這篇文章以及代碼實(shí)例问顷,都采用了 ES6 或者 ES7 的寫法昂秃;
  • 對(duì)于一些基本概念不再進(jìn)行科普。適合有 React 初級(jí)經(jīng)驗(yàn)的讀者閱讀择诈;
  • 如果有任何問(wèn)題械蹋,歡迎留言交流。

基于 Class 的組件最佳實(shí)踐(Class Based Components)

基于 Class 的組件是狀態(tài)化的羞芍,包含有自身方法哗戈、生命周期函數(shù)、組件內(nèi)狀態(tài)等荷科。最佳實(shí)踐包括但不限于以下一些內(nèi)容:

1)引入 CSS 依賴 (Importing CSS)

我很喜歡 CSS in JavaScript 這一理念唯咬。在 React 中,我們可以為每一個(gè) React 組件引入相應(yīng)的 CSS 文件畏浆,這一“夢(mèng)想”成為了現(xiàn)實(shí)胆胰。在下面的代碼示例,我把 CSS 文件的引入與其他依賴隔行分開刻获,以示區(qū)別:

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

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

當(dāng)然蜀涨,這并不是真正意義上的 CSS in JS,具體實(shí)現(xiàn)其實(shí)社區(qū)上有很多方案蝎毡。我的 Github 上 fork 了一份各種 CSS in JS 方案的多維度對(duì)比厚柳,感興趣的讀者可以點(diǎn)擊這里

2)設(shè)定初始狀態(tài)(Initializing State)

在編寫組件過(guò)程中沐兵,一定要注意初始狀態(tài)的設(shè)定别垮。利用 ES6 模塊化的知識(shí),我們確保該組件暴露都是 “export default” 形式扎谎,方便其他模塊(組件)的調(diào)用和團(tuán)隊(duì)協(xié)作碳想。

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

3)設(shè)定 propTypes 和 defaultProps

propTypes 和 defaultProps 都是組件的靜態(tài)屬性烧董。在組件的代碼中,這兩個(gè)屬性的設(shè)定位置越高越好胧奔。因?yàn)檫@樣方便其他閱讀代碼者或者開發(fā)者自己 review逊移,一眼就能看到這些信息。這些信息就如同組件文檔一樣龙填,對(duì)于理解或熟悉當(dāng)前組件非常重要螟左。

同樣,原則上觅够,你編寫的組件都需要有 propTypes 屬性胶背。如同以下代碼:

export default class ProfileContainer extends Component {
    state = { expanded: false }

    static propTypes = {
        model: React.PropTypes.object.isRequired,
        title: React.PropTypes.string
    }

    static defaultProps = {
        model: {
            id: 0
        },
        title: 'Your Name'
    }

Functional Components 是指沒(méi)有狀態(tài)、沒(méi)有方法喘先,純組件钳吟。我們應(yīng)該最大限度地編寫和使用這一類組件。這類組件作為函數(shù)窘拯,其參數(shù)就是 props, 我們可以合理設(shè)定初始狀態(tài)和賦值红且。

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

4)組件方法(Methods)

在編寫組件方法時(shí),尤其是你將一個(gè)方法作為 props 傳遞給子組件時(shí)涤姊,需要確保 this 的正確指向暇番。我們通常使用 bind 或者 ES6 箭頭函數(shù)來(lái)達(dá)到此目的。

export default class ProfileContainer extends Component {
    state = { expanded: false }

    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í)現(xiàn)方式多種多樣,我專門有一片文章來(lái)對(duì)比 React 中對(duì)于組件 this 的綁定恨课,可以點(diǎn)擊此處參考舆乔。

5)setState 接受一個(gè)函數(shù)作為參數(shù)(Passing setState a Function)

在上面的代碼示例中,我們使用了:

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

這里剂公,關(guān)于 setState hook 函數(shù)希俩,其實(shí)有一個(gè)非常“有意思”的問(wèn)題纲辽。React 在設(shè)計(jì)時(shí)颜武,為了性能上的優(yōu)化,采用了 Batch 思想拖吼,會(huì)收集“一波” state 的變化鳞上,統(tǒng)一進(jìn)行處理。就像瀏覽器繪制文檔的實(shí)現(xiàn)一樣绿贞。所以 setState 之后因块,state 也許不會(huì)馬上就發(fā)生變化橘原,這是一個(gè)異步的過(guò)程籍铁。

這說(shuō)明涡上,我們要謹(jǐn)慎地在 setState 中使用當(dāng)前的 state,因?yàn)楫?dāng)前的state 也許并不可靠拒名。
為了規(guī)避這個(gè)問(wèn)題吩愧,我們可以這樣做:

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

我們給 setState 方法傳遞一個(gè)函數(shù),函數(shù)參數(shù)為上一刻 state增显,便保證setState 能夠立刻執(zhí)行雁佳。

關(guān)于 React setState 的設(shè)計(jì), Eric Elliott 也曾經(jīng)這么噴過(guò):setState() Gate同云,并由此展開了多方“撕逼”糖权。作為圍觀群眾,我們?cè)诔怨系耐瑫r(shí)炸站,一定會(huì)在大神論道當(dāng)中收獲很多思想星澳,建議閱讀。

如果你對(duì) setState 方法的異步性還有困惑旱易,可以同我討論禁偎,這里不再展開。

6)合理利用解構(gòu)(Destructuring Props)

這個(gè)其實(shí)沒(méi)有太多可說(shuō)的阀坏,仔細(xì)觀察代碼吧:我們使用了解構(gòu)賦值如暖。除此之外,如果一個(gè)組件有很多的 props 的話忌堂,每個(gè) props 應(yīng)該都另起一行盒至,這樣書寫上和閱讀性上都有更好的體驗(yàn)。

export default class ProfileContainer extends Component {
    state = { expanded: false }

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

7)使用修飾器(Decorators)

這一條是對(duì)使用 mobx 的開發(fā)者來(lái)說(shuō)的士修。如果你不懂 mobx妄迁,可以大體掃一眼。
我們強(qiáng)調(diào)使用 ES next decorate 來(lái)修飾我們的組件李命,如同:

@observer
export default class ProfileContainer extends Component {

使用修飾器更加靈活且可讀性更高登淘。即便你不使用修飾器,也需要如此暴露你的組件:

class ProfileContainer extends Component {
    // Component code
}
export default observer(ProfileContainer)

8)閉包(Closures)

一定要盡量避免以下用法:

<input
    type="text"
    value={model.name}
    // onChange={(e) => { model.name = e.target.value }}
    // ^ Not this. Use the below:
    onChange={this.handleChange}
    placeholder="Your Name"/>

不要:

onChange = {(e) => { model.name = e.target.value }}

而是:

onChange = {this.handleChange}

原因其實(shí)很簡(jiǎn)單封字,每次父組件 render 的時(shí)候黔州,都會(huì)新建一個(gè)新的函數(shù)并傳遞給 input。
如果 input 是一個(gè) React 組件阔籽,這會(huì)粗暴地直接導(dǎo)致這個(gè)組件的 re-render流妻,需要知道,Reconciliation 可是 React 成本最高的部分笆制。

另外绅这,我們推薦的方法,會(huì)使得閱讀在辆、調(diào)試和更改更加方便证薇。

9)JSX中的條件判別(Conditionals in JSX)

真正寫過(guò) React 項(xiàng)目的同學(xué)一定會(huì)明白度苔,JSX 中可能會(huì)存在大量的條件判別,以達(dá)到根據(jù)不同的情況渲染不同組件形態(tài)的效果浑度。
就像下圖這樣:

返例

這樣的結(jié)果是不理想的寇窑。我們丟失了代碼的可讀性,也使得代碼組織顯得混亂異常箩张。多層次的嵌套也是應(yīng)該避免的甩骏。

針對(duì)于此,有很對(duì)類庫(kù)來(lái)解決 JSX-Control Statements 此類問(wèn)題先慷,但是與其引入第三方類庫(kù)的依賴饮笛,還不如我們先自己嘗試探索解決問(wèn)題。

此時(shí)论熙,是不是有點(diǎn)懷念if...else缎浇?
我們可以使用大括號(hào)內(nèi)包含立即執(zhí)行函數(shù)IIFE,來(lái)達(dá)到使用 if...else 的目的:

解決思路

當(dāng)然赴肚,大量使用立即執(zhí)行函數(shù)會(huì)造成性能上的損失素跺。所以,考慮代碼可讀性上的權(quán)衡誉券,還是有必要好好斟酌的指厌。
我更加建議的做法是分解此組件,因?yàn)檫@個(gè)組件的邏輯已經(jīng)過(guò)于復(fù)雜而臃腫了踊跟。如何分解踩验?請(qǐng)看我這篇文章。

總結(jié)

其實(shí)所謂 React “最佳實(shí)踐”商玫,想必每個(gè)團(tuán)隊(duì)都有自己的一套“心得”箕憾,哪里有一個(gè)統(tǒng)一套? 本文指出的幾種方法未必對(duì)任何讀者都適用拳昌。針對(duì)不同的代碼風(fēng)格袭异,開發(fā)習(xí)慣,擁有自己團(tuán)隊(duì)一套“最佳實(shí)踐”是很有必要的炬藤。從另一方面御铃,也說(shuō)明了 React 技術(shù)棧本身的靈活于強(qiáng)大。

另外沈矿,這篇文章并不是我原創(chuàng)上真,而是翻譯了Our Best Practices for Writing React Components一文,并在此基礎(chǔ)上進(jìn)行了較大幅度擴(kuò)展羹膳。

如果您對(duì)React生態(tài)有興趣睡互,同樣推薦我的其他幾篇文章:

Happy Coding!

PS:
作者Github倉(cāng)庫(kù)知乎問(wèn)答鏈接
歡迎各種形式交流。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市就珠,隨后出現(xiàn)的幾起案子寇壳,更是在濱河造成了極大的恐慌,老刑警劉巖嗓违,帶你破解...
    沈念sama閱讀 216,744評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異图贸,居然都是意外死亡蹂季,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,505評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門疏日,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)偿洁,“玉大人,你說(shuō)我怎么就攤上這事沟优√樽蹋” “怎么了?”我有些...
    開封第一講書人閱讀 163,105評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵挠阁,是天一觀的道長(zhǎng)宾肺。 經(jīng)常有香客問(wèn)我,道長(zhǎng)侵俗,這世上最難降的妖魔是什么锨用? 我笑而不...
    開封第一講書人閱讀 58,242評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮隘谣,結(jié)果婚禮上增拥,老公的妹妹穿的比我還像新娘。我一直安慰自己寻歧,他們只是感情好掌栅,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,269評(píng)論 6 389
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著码泛,像睡著了一般猾封。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上噪珊,一...
    開封第一講書人閱讀 51,215評(píng)論 1 299
  • 那天忘衍,我揣著相機(jī)與錄音,去河邊找鬼卿城。 笑死枚钓,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瑟押。 我是一名探鬼主播搀捷,決...
    沈念sama閱讀 40,096評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了嫩舟?” 一聲冷哼從身側(cè)響起氢烘,我...
    開封第一講書人閱讀 38,939評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎家厌,沒(méi)想到半個(gè)月后播玖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,354評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡饭于,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,573評(píng)論 2 333
  • 正文 我和宋清朗相戀三年蜀踏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片掰吕。...
    茶點(diǎn)故事閱讀 39,745評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡果覆,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出殖熟,到底是詐尸還是另有隱情局待,我是刑警寧澤,帶...
    沈念sama閱讀 35,448評(píng)論 5 344
  • 正文 年R本政府宣布菱属,位于F島的核電站钳榨,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏纽门。R本人自食惡果不足惜重绷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,048評(píng)論 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望膜毁。 院中可真熱鬧昭卓,春花似錦、人聲如沸瘟滨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,683評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)杂瘸。三九已至倒淫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間败玉,已是汗流浹背敌土。 一陣腳步聲響...
    開封第一講書人閱讀 32,838評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留运翼,地道東北人返干。 一個(gè)月前我還...
    沈念sama閱讀 47,776評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像血淌,于是被迫代替她去往敵國(guó)和親矩欠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子财剖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,652評(píng)論 2 354

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

  • 本筆記基于React官方文檔,當(dāng)前React版本號(hào)為15.4.0癌淮。 1. 安裝 1.1 嘗試 開始之前可以先去co...
    Awey閱讀 7,700評(píng)論 14 128
  • 最近看了一本關(guān)于學(xué)習(xí)方法論的書躺坟,強(qiáng)調(diào)了記筆記和堅(jiān)持的重要性。這幾天也剛好在學(xué)習(xí)React乳蓄,所以我打算每天堅(jiān)持一篇R...
    gaoer1938閱讀 1,675評(píng)論 0 5
  • GUIDS 第一章 為什么使用React咪橙? React 一個(gè)提供了用戶接口的JavaScript庫(kù)。 誕生于Fac...
    jplyue閱讀 3,532評(píng)論 1 11
  • 深入JSX date:20170412筆記原文其實(shí)JSX是React.createElement(componen...
    gaoer1938閱讀 8,061評(píng)論 2 35
  • 空中飛舞的落葉虚倒,是風(fēng)的追逐美侦,還是樹的拋棄…… 不知曉,或許過(guò)于癡情裹刮。用一生為樹干支起了一片綠蔭音榜,甘愿為他忍受烈日的...
    LovLe閱讀 290評(píng)論 0 2