MuseFind:編寫React組件的最佳實現
React 起源于 非死book 的內部項目,因為該公司對市場上所有 JavaScript MVC框架,都不滿意,就決定自己寫一套,用來架設 Instagram 的網站。做出來以后,發現這套東西很好用,就在2013年5月 開源 了。
由于React的設計思想極其獨特,屬于革命性創新,性能出眾,代碼邏輯卻非常簡單。所以,越來越多的人開始關注和使用,認為它可能是將來Web開發的主流工具。
當我第一次開始寫React代碼時,我記得看到過許多不同的編寫組件的方法,各教程之間有很大的不同。雖然自那時以來框架已經相當成熟,但似乎還沒有一個確定“正確”編寫的方式。
在過去一年里,在 MuseFind ,我們的團隊編寫了很多React組件。我們已經逐漸完善了方法,直到我們滿意為止。
本指南代表我們建議的最佳做法。我們認為本文對新手和老手都有所幫助。
閱讀本文之前,讀者需要注意以下幾點:
- 我們使用ES6和ES7語法。
- 如果你不確定展示型組件和容器組件之間的區別,我們建議你先閱讀 這篇文章 。
- 如果您有任何建議問題或反饋,請在原文的評論區中告訴我們。
基于類的組件
基于類的組件是有狀態的,或許還包含方法。我們盡可能少地使用它們,但它們也有自己的位置。
讓我們逐行構建我們的組件。
導入CSS
import React, {Component} from 'react'
import {observer} from 'mobx-react'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
理論上,我喜歡 CSS in JavaScript 。但它仍然是一個新的想法,還沒有出現一個成熟的解決方案。在此之前,我們將一個CSS文件導入到每個組件。
我們還通過換行將依賴導入與本地導入分開。
初始化狀態
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 }
如果你使用ES6(ES7不適用),在構造函數中初始化狀態。否則,使用專用于ES7的方法。更多信息在 這篇文章 。
我們還要確保將我們的類導出為默認類。
propTypes和defaultProps
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是靜態屬性,在組件代碼中聲明的優先級盡可能高。由于它們作為文檔,因此它們應該對其他讀取文件的開發者可見。
所有的組件應該有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 })
}
使用類組件,當將方法傳遞給子組件時,必須確保它們在調用時具有正確的this。通常通過傳遞this.handleSubmit.bind(this)到子組件來實現。
我們認為這種方法更簡潔也更容易,通過ES6的箭頭函數自動保持正確的上下文。
解構props
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>
)
}
}
具有多個props的組件,每個props應該占據單獨一個行,如上所示。
裝飾器
@observer
export default class ProfileContainer extends Component {
如果你使用像 mobx 這樣的東西,你可以將類組件裝飾成這樣:這與將組件傳遞到函數相同。
裝飾器 通過靈活、可讀的方式來修改組件功能。我們廣泛地使用裝飾器,配合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"/>
這就是為什么每次父組件渲染時,創建一個新的函數并傳遞給輸入的原因。
如果輸入是React組件,這將自動觸發它重新渲染,而不管它的其他props是否實際改變。
調和算法(Reconciliation)是React最耗時的部分。不要讓它比所需更難!此外,傳遞類的方法更容易閱讀、調試和更改。
這是我們的完整組件:
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>
)
}
}
函數組件
這些組件沒有狀態、方法。它們是純粹的,簡單的。因此要盡可能經常使用它們。
propTypes
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
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>
)
}
我們的組件是一個函數,它的Props作為其參數。我們可以這樣擴展:
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>
)
}
注意,我們也可以使用默認參數作為defaultProps以高度可讀的方式。如果展開沒有定義的話,我們將其設置為false。(這是一個有點強迫的例子,因為它是一個布爾,但非常有用,以避免“無法讀取未定義”錯誤與對象)。
避免使用以下ES6語法:
const ExpandableForm = ({ onExpand, expanded, children }) => {
看起來很現代,但此處的函數實際上是未命名的。
如果你的Babel設置正確,這個名字的缺失不會成為一個問題:但如果不是,任何錯誤將<<anonymous>>中顯示,對調試而言,是一個非常嚴重的問題。
未命名的函數也可能導致Jest(一個React測試庫)的問題。由于潛在的難以理解的bug,以及并沒有什么真正的好處,我們建議使用function而不是const。
包裝
因為你不能使用裝飾器和功能組件,你只需將函數作為參數傳遞給它:
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)
這是我們的完整組件:
JSX條件
很可能你要做很多條件渲染。這里是你想避免的地方:
(點擊放大圖像)
這是我在MuseFind早期寫的實際代碼,饒恕我吧。
不,嵌套的三元運算并不是一個好主意。
有一些庫解決了這個問題( JSX控制語句 ),但是,而不是引入另一個依賴,我們解決了這種應對復雜條件的方法:
(點擊放大圖像)
以上所示是重構版本。
使用花括號括起一個 IIFE ,然后把你的if語句置于里面,返回任何你想要的渲染。請注意,這樣的IIFE可能會導致性能下降,但在大多數情況下,它不會嚴重到以致失去可讀性。
此外,當你只想渲染一個條件上的元素,而不是這樣做:
{
isTrue
? <p>True!</p>
: <none/>
}
使用short-circuit賦值:
{
isTrue &&
<p>True!</p>
}
來自:http://www.infoq.com/cn/articles/our-best-practices-for-writing-react-components