現(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)有興趣睡互,同樣推薦我的其他幾篇文章:
- React 組件設(shè)計(jì)和分解思考
- 做出Uber移動(dòng)網(wǎng)頁(yè)版還不夠 極致性能打造才見真章
- 解析Twitter前端架構(gòu) 學(xué)習(xí)復(fù)雜場(chǎng)景數(shù)據(jù)設(shè)計(jì)
- React Conf 2017 干貨總結(jié)1: React + ES next = ?
- React+Redux打造“NEWS EARLY”單頁(yè)應(yīng)用 一個(gè)項(xiàng)目理解最前沿技術(shù)棧真諦
- 一個(gè)react+redux工程實(shí)例
- ......
Happy Coding!
PS:
作者Github倉(cāng)庫(kù) 和 知乎問(wèn)答鏈接
歡迎各種形式交流。