編寫React組件的最佳實踐
在我***次編寫 React 代碼的時候,我見發(fā)現(xiàn)許多不同的方法可以用來編寫組件,不同教程教授的內(nèi)容也大不相同。盡管從那時候起框架已經(jīng)相當(dāng)成熟,但并沒有一種固定的“正確”方式指導(dǎo)。
在 MuseFind 工作的一年里,我們的團隊編寫了許多 React 組件,后期我們對方法進行了優(yōu)化直到滿意為止。
本指南描述了我們推薦的***實踐,不管你是一名初學(xué)者還是有經(jīng)驗的老手,希望它能對你有所幫助。
在我們開始之前,有幾個地方要注意一下:
-
我們使用的是 ES6 和 ES7 的語法。
-
如果你對于現(xiàn)實和容器組件兩者之間的區(qū)別不甚明了,建議首先閱讀一下這個。
-
如果有任何建議、疑問或者感想,請通過評論來讓我們知曉。
基于類的組件
基于類的組件具有豐富狀態(tài)而且可以含有方法。我們要盡可能有節(jié)制地去使用,因為它們也有特定的適用場合。
讓我們使用一行一行的代碼逐步地將我們的組件構(gòu)建起來吧。
引入 CSS
import React, {Component} from 'react'
import {observer} from 'mobx-react'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
我喜歡在 JavaScript 中操作 CSS,這在理論上這樣做是可行的。不過它仍然是一種新的創(chuàng)意,還沒出現(xiàn)切實可行的解決方案。不過在此之前,我們可以先為每一個組件引入一個 CSS 文件。
我們也通過另寫一行將依賴引入從本地引入獨立了出來。
狀態(tài)初始化
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 }
propTypes 和 defaultProps
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 }
static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
propTypes 和 defaultProps 是靜態(tài)屬性,要在組件代碼中盡可能高的位置進行聲明。它們作為文檔放在醒目的位置,其他開發(fā)者閱讀此文件時能立即看到。
所有的組件都應(yīng)該有 propTypes。
方法
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 }
static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
handleSubmit = (e) => {
e.preventDefault()
this.props.model.save()
}
handleNameChange = (e) => {
this.props.model.name = e.target.value
}
handleExpand = (e) => {
e.preventDefault()
this.setState({ expanded: !this.state.expanded })
}
有了類組件,在你想子組件傳遞方法時,就得去確認它們在被調(diào)用到時所持有的 this 對象是正確的。這個一般可以通過將 this.handleSubmit.bind(this) 傳遞給子組件來達成。
我們認為這種方式更加干凈且容易,借助 ES6 的箭頭函數(shù)可以自動地維護好正確的上線文。
屬性析構(gòu)
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 }
static propTypes = {
model: React.PropTypes.object.isRequired,
title: React.PropTypes.string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
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() {
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> ) } }
擁有許多屬性的組件要讓每個屬性都另起一行,如上所示。
裝飾器
@observer
export default class ProfileContainer extends Component {
如果你使用了一些像 mobx 的東西,就可以像上面這樣對類組件進行裝飾 — 這樣做跟將組件傳遞給一個函數(shù)是一樣的效果。
裝飾器是一種用來修改組件功能的靈活且可讀性好的方式。我們對其進行了廣泛的運用,包括 mobx 還有我們自己的 mobx-models 庫。
如果你不想使用裝飾器,可以這樣做:
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"/>
原因: 每次父組件渲染時,都會有一個新的函數(shù)被創(chuàng)建并傳遞給輸入。
如果輸入是一個 React 組件,不管它的其它屬性實際是否已經(jīng)發(fā)生了變化,都會自動地觸發(fā)讓它重新渲染。
調(diào)和是 React 中消耗最昂貴的部分,因此不要讓它的計算難度超過所需! 另外,傳遞一個類方法更容易閱讀、調(diào)試和修改。
如下是完整的組件代碼:
import React, {Component} from 'react'
import {observer} from 'mobx-react'
// 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: React.PropTypes.object.isRequired,
title: React.PropTypes.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> ) } }
函數(shù)式組件
這些組件沒有狀態(tài)和方法。它們就是單純的組件,容易理解。要盡可能常去使用它們。
propTypes
import React from 'react'
import {observer} from 'mobx-react'
import './styles/Form.css'
const expandableFormRequiredProps = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool
}
// Component declaration
ExpandableForm.propTypes = expandableFormRequiredProps
這里,我們將 propTypes 分配給了頂部一行的變量。在組件聲明的下面,我們對它們進行了正常的分配。
對 Props 和 defaultProps 進行析構(gòu)
import React from 'react'
import {observer} from 'mobx-react'
import './styles/Form.css'
const expandableFormRequiredProps = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool
}
function ExpandableForm(props) {
return (
<form style={props.expanded ? {height: 'auto'} : {height: 0}}> {props.children} <button onClick={props.onExpand}>Expand</button> </form>
)
}
我的組件是一個函數(shù),因此可以將它的屬性看做是參數(shù)。我們可以像下面這樣對它們進行擴展:
import React from 'react'
import {observer} from 'mobx-react'
import './styles/Form.css'
const expandableFormRequiredProps = {
onExpand: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool
}
function ExpandableForm({ onExpand, expanded = false, children }) {
return (
<form style={ expanded ? { height: 'auto' } : { height: 0 } }> {children} <button onClick={onExpand}>Expand</button> </form>
)
}
注意,我們也能以一種高度可讀的方式使用默認參數(shù)來扮演 defaultProps 的角色。如果 expanded 是 undefined, 我們就會將其設(shè)置為 false。 (這個例子有點勉強,因為是一個布爾值,不過本身對于避免對象的“Cannot read <property> of undefined“這樣的錯誤是很有用的)。
要避免如下這種 ES6 語法:
const ExpandableForm = ({ onExpand, expanded, children }) => {
看著非?,F(xiàn)代,不過這里的函數(shù)實際上沒有被命令。
這樣子的名稱在 Bable 進行了正確的設(shè)置的情況下是可行的 — 但如果沒有正確設(shè)置,任何錯誤都會以在<<anonymous>>中出現(xiàn)的方式顯示,調(diào)試起來相當(dāng)麻煩。
無名的函數(shù)也會在 Jest 這個 React 測試庫中引發(fā)問題。為了避免潛在的復(fù)雜問題出現(xiàn),我們建議使用 function 而不是 const。
封裝
因為在函數(shù)式組件中不能使用裝飾器,所以你可以簡單地將它傳遞到函數(shù)中充當(dāng)參數(shù):
import React from 'react'
import {observer} from 'mobx-react'
import './styles/Form.css'
const expandableFormRequiredProps = {
onExpand: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool
}
function ExpandableForm({ onExpand, expanded = false, children }) {
return (
<form style={ expanded ? { height: 'auto' } : { height: 0 } }> {children} <button onClick={onExpand}>Expand</button> </form>
)
}
ExpandableForm.propTypes = expandableFormRequiredProps
export default observer(ExpandableForm)
如下是完整的組件代碼:
import React from 'react'
import {observer} from 'mobx-react'
// Separate local imports from dependencies
import './styles/Form.css'
// Declare propTypes here as a variable, then assign below function declaration
// You want these to be as visible as possible
const expandableFormRequiredProps = {
onSubmit: React.PropTypes.func.isRequired,
expanded: React.PropTypes.bool
}
// Destructure props like so, and use default arguments as a way of setting defaultProps
function ExpandableForm({ onExpand, expanded = false, children }) {
return (
<form style={ expanded ? { height: 'auto' } : { height: 0 } }> {children} <button onClick={onExpand}>Expand</button> </form>
)
}
// Set propTypes down here to those declared above
ExpandableForm.propTypes = expandableFormRequiredProps
// Wrap the component instead of decorating it
export default observer(ExpandableForm)
JSX 中的條件分支
你會有不少機會去做許多條件分支渲染。如下是你想要去避免的情況:
嵌套的三元組并非不是好主意。
有一些庫可以解決這個問題 (JSX-Control Statements),不過相比引入額外的依賴,通過如下這種方式解決復(fù)雜條件分支問題要更好:
使用花括弧封裝一個 IIFE, 然后在里面放入 if 語句,可以返回任何你想要渲染的東西。注意像這樣的 IIFE 對性能會有影響,不過在大多數(shù)情況中還不足以讓我們?yōu)榇诉x擇丟掉可讀性。
還有就是當(dāng)你只想要在一個條件分支中渲染一個元素時,比起這樣做…
{
isTrue
? <p>True!</p>
: <none/>
}
… 使用短路寫法更劃算:
{
isTrue &&
<p>True!</p>
}