現在前端程式設計師都知道,React 是元件化的。當我開始學習 React 的時候,我記得當時已經存在了很多不同編寫元件的方式了。如今,React
社群已經愈發成熟,但是對於元件正確編寫姿勢卻沒有一個相對完備的指導。
這篇文章僅從作者的觀點出發,來談一談我們究竟應該如何來寫高質量的 React 元件。
在開始前,需要說明以下幾個問題:
- 這篇文章以及程式碼例項,都採用了 ES6 或者 ES7 的寫法;
- 對於一些基本概念不再進行科普。適合有 React 初級經驗的讀者閱讀;
- 如果有任何問題,歡迎留言交流。
基於 Class 的元件最佳實踐(Class Based Components)
基於 Class 的元件是狀態化的,包含有自身方法、生命週期函式、元件內狀態等。最佳實踐包括但不限於以下一些內容:
1)引入 CSS 依賴 (Importing CSS)
我很喜歡 CSS in JavaScript 這一理念。在 React 中,我們可以為每一個 React 元件引入相應的 CSS 檔案,這一“夢想”成為了現實。在下面的程式碼示例,我把 CSS 檔案的引入與其他依賴隔行分開,以示區別:
import React, {Component} from 'react'
import {observer} from 'mobx-react'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'複製程式碼
當然,這並不是真正意義上的 CSS in JS,具體實現其實社群上有很多方案。我的 Github 上 fork 了一份各種 CSS in JS 方案的多維度對比,感興趣的讀者可以點選這裡。
2)設定初始狀態(Initializing State)
在編寫元件過程中,一定要注意初始狀態的設定。利用 ES6 模組化的知識,我們確保該元件暴露都是 “export default” 形式,方便其他模組(元件)的呼叫和團隊協作。
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)設定 propTypes 和 defaultProps
propTypes 和 defaultProps 都是元件的靜態屬性。在元件的程式碼中,這兩個屬性的設定位置越高越好。因為這樣方便其他閱讀程式碼者或者開發者自己 review,一眼就能看到這些資訊。這些資訊就如同元件文件一樣,對於理解或熟悉當前元件非常重要。
同樣,原則上,你編寫的元件都需要有 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 是指沒有狀態、沒有方法,純元件。我們應該最大限度地編寫和使用這一類元件。這類元件作為函式,其引數就是 props, 我們可以合理設定初始狀態和賦值。
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)
在編寫元件方法時,尤其是你將一個方法作為 props 傳遞給子元件時,需要確保 this 的正確指向。我們通常使用 bind 或者 ES6 箭頭函式來達到此目的。
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 })
}複製程式碼
當然,這並不是唯一做法。實現方式多種多樣,我專門有一片文章來對比 React 中對於元件 this 的繫結,可以點選此處參考。
5)setState 接受一個函式作為引數(Passing setState a Function)
在上面的程式碼示例中,我們使用了:
this.setState({ expanded: !this.state.expanded })複製程式碼
這裡,關於 setState hook 函式,其實有一個非常“有意思”的問題。React 在設計時,為了效能上的優化,採用了 Batch 思想,會收集“一波” state 的變化,統一進行處理。就像瀏覽器繪製文件的實現一樣。所以 setState 之後,state 也許不會馬上就發生變化,這是一個非同步的過程。
這說明,我們要謹慎地在 setState 中使用當前的 state,因為當前的state 也許並不可靠。
為了規避這個問題,我們可以這樣做:
this.setState(prevState => ({ expanded: !prevState.expanded }))複製程式碼
我們給 setState 方法傳遞一個函式,函式引數為上一刻 state,便保證setState 能夠立刻執行。
關於 React setState 的設計, Eric Elliott 也曾經這麼噴過:setState() Gate,並由此展開了多方“撕逼”。作為圍觀群眾,我們在吃瓜的同時,一定會在大神論道當中收穫很多思想,建議閱讀。
如果你對 setState 方法的非同步性還有困惑,可以同我討論,這裡不再展開。
6)合理利用解構(Destructuring Props)
這個其實沒有太多可說的,仔細觀察程式碼吧:我們使用瞭解構賦值。除此之外,如果一個元件有很多的 props 的話,每個 props 應該都另起一行,這樣書寫上和閱讀性上都有更好的體驗。
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)
這一條是對使用 mobx 的開發者來說的。如果你不懂 mobx,可以大體掃一眼。
我們強調使用 ES next decorate 來修飾我們的元件,如同:
@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}複製程式碼
原因其實很簡單,每次父元件 render 的時候,都會新建一個新的函式並傳遞給 input。
如果 input 是一個 React 元件,這會粗暴地直接導致這個元件的 re-render,需要知道,Reconciliation 可是 React 成本最高的部分。
另外,我們推薦的方法,會使得閱讀、除錯和更改更加方便。
9)JSX中的條件判別(Conditionals in JSX)
真正寫過 React 專案的同學一定會明白,JSX 中可能會存在大量的條件判別,以達到根據不同的情況渲染不同元件形態的效果。
就像下圖這樣:
這樣的結果是不理想的。我們丟失了程式碼的可讀性,也使得程式碼組織顯得混亂異常。多層次的巢狀也是應該避免的。
針對於此,有很對類庫來解決 JSX-Control Statements 此類問題,但是與其引入第三方類庫的依賴,還不如我們先自己嘗試探索解決問題。
此時,是不是有點懷念if...else?
我們可以使用大括號內包含立即執行函式IIFE,來達到使用 if...else 的目的:
當然,大量使用立即執行函式會造成效能上的損失。所以,考慮程式碼可讀性上的權衡,還是有必要好好斟酌的。
我更加建議的做法是分解此元件,因為這個元件的邏輯已經過於複雜而臃腫了。如何分解?請看我這篇文章。
總結
其實所謂 React “最佳實踐”,想必每個團隊都有自己的一套“心得”,哪裡有一個統一套? 本文指出的幾種方法未必對任何讀者都適用。針對不同的程式碼風格,開發習慣,擁有自己團隊一套“最佳實踐”是很有必要的。從另一方面,也說明了 React 技術棧本身的靈活於強大。
另外,這篇文章並不是我原創,而是翻譯了Our Best Practices for Writing React Components一文,並在此基礎上進行了較大幅度擴充套件。
如果您對React生態有興趣,同樣推薦我的其他幾篇文章:
- React 元件設計和分解思考
- 做出Uber移動網頁版還不夠 極致效能打造才見真章
- 解析Twitter前端架構 學習複雜場景資料設計
- React Conf 2017 乾貨總結1: React + ES next = ♥
- React+Redux打造“NEWS EARLY”單頁應用 一個專案理解最前沿技術棧真諦
- 一個react+redux工程例項
- ......
Happy Coding!